API and MCP reference
The same screening engine this website calls, reached by your own software. 11 HTTP endpoints and one MCP endpoint, over one set of actions — there is no second implementation behind either, so an answer here and an answer in the interface cannot disagree.
What this page is notNot on sale
It is a reference, not a way in. Programmatic access comes with the API & Automation plan, and that plan is not on sale — there is no checkout for it and nothing on this site will issue you a credential. The accounts that hold keys today were provisioned by us directly, under terms agreed with them; there is no self-serve route onto this plan. This page is written for them, and for anyone judging whether the surface is worth building against. No date is promised here; the pricing page is where the plan’s status is stated, and where the plans that are arranged rather than bought are listed.
Authenticating
One header, and nothing else. A key is not accepted in a query string or in a second header: a query string is written to access logs by default, and a credential that can arrive two ways is one a future reader has to check twice before believing it cannot arrive a third.
curl https://gridcite.com/api/agent/projects \
-H "Authorization: Bearer YOUR_GRIDCITE_API_KEY"Keys are created and revoked in the account panel, which is cookie-authenticated in a browser. A key cannot mint another key — that is deliberate, because a credential that renews itself makes revocation mean nothing. The full secret is shown once, at the moment it is created, and only a hash is stored afterwards; “show it to me again” is a request nothing can satisfy, so capture it then.
Arguments to a GET travel in the query string, where every value is text — ?decision=advance, ?includeArchived=true. Arguments to a POST or PATCH travel in a JSON body. A value named in the path is read from the path and cannot be overridden by either.
Endpoints
Every description below is the tool’s own, imported rather than rewritten, so what you read here is exactly what a model reading tools/list is told. Each one states the boundary as plainly as the capability, because a description that oversells is not a marketing problem — it is a wrong answer delivered at speed.
POST /api/agent/projectscreate_projectCreate a development project: its type, its target size, the counties it is looking in, and the developer's own screening thresholds. Returns the project's requirements with every unstated field as an explicit unknown. It does NOT screen anything and does not find land — a new project has no sites until they are added with save_candidate, and the result's recommendation is deliberately "none". It will not invent a target, a county or a criterion that was not given: unstated inputs come back as open questions to ask the developer, never as assumed values.
namestring · body · requiredWhat the developer calls this project. 120 characters or fewer, and not blank.
projectTypeone of: bess, custom, datacenter, distributionsolar, evcharging, general, industrial, largedistributionsolar, solar · body · requiredWhich screening profile the project's sites are judged under.
targetMwnumber or null · body · optionalTarget size in MW, or null if not stated.
targetMwhnumber or null · body · optionalStorage energy target in MWh, or null.
targetAcresnumber or null · body · optionalBuildable acreage needed, or null.
targetCodstring or null · body · optionalTarget commercial operation date as YYYY-MM-DD, or null. Must be a real calendar date.
geographyNotestring or null · body · optionalFree text about the market.
countiesarray of string · body · optionalCounties in scope. Each entry must be a non-blank string.
utilityNotestring or null · body · optionalThe assumed utility, if any.
jurisdictionNotestring or null · body · optionalNotes on the permitting authority.
siteControlone of: none, loi, option, lease, owned, null · body · optionalHow much of the site the developer controls, or null if unstated. Weakest to strongest: none, loi, option, lease, owned. "none" is a stated answer — the developer holds nothing — and is a different claim from null, which means they have not said.
criteriaobject · body · optionalThe developer's OWN screening thresholds, e.g. { "minAcres": 80 }. Unstated criteria fall back to GridCite's template and every result says which is which.
notesstring or null · body · optionalFree text.
GET /api/agent/projectslist_projectsList the projects on this account: name, type, counties, when each was last changed, and what each one still has not answered. This is how a caller DISCOVERS the project ids every other tool needs. It does NOT screen anything: no scores, no verdicts, no site counts, because a verdict frozen into a listing can contradict the live evidence by the time somebody acts on it — ask screen_project about the project you chose. Archived projects are excluded unless includeArchived is true. An empty list is a real answer, not a failure.
includeArchivedboolean · query · optionalInclude projects that have been put away. Defaults to false.
PATCH /api/agent/projects/{projectId}update_projectChange a project's stated inputs or criteria. Only the fields sent are written; an omitted field is left alone and an explicit null CLEARS it. It does NOT screen, does not touch the project's saved sites, and cannot move a project to another account. Send archivedAt to archive; nothing here deletes a project or a decision.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
namestring · body · optionalWhat the developer calls this project. 120 characters or fewer, and not blank.
projectTypeone of: bess, custom, datacenter, distributionsolar, evcharging, general, industrial, largedistributionsolar, solar · body · optionalWhich screening profile the project's sites are judged under.
targetMwnumber or null · body · optionalTarget size in MW, or null if not stated.
targetMwhnumber or null · body · optionalStorage energy target in MWh, or null.
targetAcresnumber or null · body · optionalBuildable acreage needed, or null.
targetCodstring or null · body · optionalTarget commercial operation date as YYYY-MM-DD, or null. Must be a real calendar date.
geographyNotestring or null · body · optionalFree text about the market.
countiesarray of string · body · optionalCounties in scope. Each entry must be a non-blank string.
utilityNotestring or null · body · optionalThe assumed utility, if any.
jurisdictionNotestring or null · body · optionalNotes on the permitting authority.
siteControlone of: none, loi, option, lease, owned, null · body · optionalHow much of the site the developer controls, or null if unstated. Weakest to strongest: none, loi, option, lease, owned. "none" is a stated answer — the developer holds nothing — and is a different claim from null, which means they have not said.
criteriaobject · body · optionalThe developer's OWN screening thresholds, e.g. { "minAcres": 80 }. Unstated criteria fall back to GridCite's template and every result says which is which.
notesstring or null · body · optionalFree text.
archivedAtstring or null · body · optional
GET /api/agent/projects/{projectId}/screeningscreen_projectScreen the sites already SAVED to a project against that project's own criteria, and return a per-site verdict with the risks, the unknowns and the evidence behind each one. It screens saved sites only — it does NOT screen a county, search for new parcels, or rank anything the developer has not added. A site that could not be matched, or that is in a county GridCite does not hold, is reported as unresolved and is never counted as a rejection. This is preliminary GIS screening: it is not a survey, a title review, an interconnection study or a permit decision, and an unknown is reported as unknown rather than assumed favourable.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
decisionone of: advance, hold, review, reject · query · optionalOptional. Return only sites the developer has put in this state. Omit for all of them. An unrecognised value is refused rather than ignored.
GET /api/agent/projects/{projectId}/candidateslist_candidatesThe project's saved sites as a roster — one row each, with the verdict, the score, the counts of fatal and material findings, and what has changed since the last assessment. Ordered advance first, then review, then rejected, then the ones that could not be resolved. It lists THIS project's sites only: it is not the countywide candidate feed and will not return parcels nobody saved. The score is a comparison aid within a list, never a verdict on a parcel.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
decisionone of: advance, hold, review, reject · query · optionalOptional. Return only sites the developer has put in this state. Omit for all of them. An unrecognised value is refused rather than ignored.
POST /api/agent/projects/{projectId}/candidatessave_candidateAdd a parcel to a project and screen it in the same call. The saved position defaults to "review", never to "advance": saving a site is an expression of interest and this tool will not record an endorsement the developer did not give. It does not create the parcel and does not confirm ownership or availability — if the identifier does not match a parcel GridCite holds, the result says so instead of screening something else.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
countystring · body · requiredThe county the parcel is in, as GridCite holds it (for example "kern").
parcelIdstring · body · requiredThe parcel identifier. GridCite's own full id, or the bare assessor APN — both are tried.
decisionone of: advance, hold, review, reject · body · optionalOptional. Defaults to "review". Only send a value the developer stated.
decisionReasonstring or null · body · optionalThe developer's words, not yours.
notesstring or null · body · optional
POST /api/agent/projects/{projectId}/candidates/decisionset_candidate_decisionRecord the DEVELOPER'S position on a saved site — advance, hold, review or reject — and re-screen it. This writes a judgement into the developer's own record, so it must only be called with a decision they actually made: it will not infer one from a screening result, and a screening verdict of "reject" is not permission to record a rejection. A rejection requires a reason, and the call is refused without one. Nothing here is reversible by a delete; a later call simply records a later position.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
countystring · body · requiredThe county the parcel is in, as GridCite holds it (for example "kern").
parcelIdstring · body · requiredThe parcel identifier. GridCite's own full id, or the bare assessor APN — both are tried.
decisionone of: advance, hold, review, reject · body · requiredThe position the developer took. Required.
decisionReasonstring or null · body · optionalRequired when rejecting. The developer's reason, in their words.
notesstring or null · body · optional
GET /api/agent/projects/{projectId}/memogenerate_memoThe development memo for one saved site, as structured data: the findings, their evidence tier, their source and the next step for each. It returns a model, not a rendered document, and it will not produce a memo for a parcel that could not be resolved — a memo built on a placeholder is the most convincing wrong artifact this product could hand somebody. The site must already be saved to the project.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
countystring · query · requiredThe county the parcel is in, as GridCite holds it (for example "kern").
parcelIdstring · query · requiredThe parcel identifier. GridCite's own full id, or the bare assessor APN — both are tried.
GET /api/agent/projects/{projectId}/utility-inquirygenerate_utility_inquiryThe standardized utility inquiry package for one saved site, plus what the developer still owes before it can be sent. It does NOT send anything, does not contact a utility, and is not an interconnection application or a capacity determination. Only solar and battery storage have a standardized package; any other project type returns a successful result with a named reason and no package, rather than an invented one.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
countystring · query · requiredThe county the parcel is in, as GridCite holds it (for example "kern").
parcelIdstring · query · requiredThe parcel identifier. GridCite's own full id, or the bare assessor APN — both are tried.
GET /api/agent/projects/{projectId}/changesget_project_changesWhat has changed about a project's saved sites since the system last assessed them, as sentences about differences. It returns no stored snapshot and no stored score: a frozen conclusion goes stale and then contradicts live screening. "Nothing changed" and "the sweep has never reached this site" are different answers here and are never merged — the second is reported as an explicit unknown. For a current verdict, call screen_project.
projectIdstring · path · requiredThe project's id — the `project.projectId` field of the envelope returned when it was created, or from list_projects. Not the project's name.
decisionone of: advance, hold, review, reject · query · optionalOptional. Return only sites the developer has put in this state. Omit for all of them. An unrecognised value is refused rather than ignored.
GET /api/agent/candidatesfind_sitesSearch GridCite's precomputed countywide candidate feed for one county and profile, ranked by score. This is DISCOVERY, not screening: it reads the same materialized build /api/candidates serves and does NOT re-score anything live or apply a project's own criteria — screen_project remains the tool for a project's saved sites, and this tool cannot see them. A county with no active build, or a county string GridCite does not recognise, both come back as not_found rather than an empty list, so a caller cannot read "nothing found" as "this land does not qualify". A site found here is not on any project until save_candidate adds it.
countystring · query · requiredThe county the parcel is in, as GridCite holds it (for example "kern").
profileone of: general, evcharging, distributionsolar, largedistributionsolar, solar, bess, datacenter, industrial · query · requiredWhich countywide screening profile to search. "custom" is never valid here — only the built profiles have a materialized county feed.
modeone of: qualified, audit · query · optionalOptional, defaults to "qualified": only sites that passed every hard rule. "audit" returns every scored site in the county regardless of status.
limitinteger · query · optionalOptional, defaults to 10. How many sites to return, 1 to 100. A value outside that range is refused.
Every response has the same shape
Success, refusal and outage all return the same fully populated envelope, so one parser handles every reply and a caller that renders it blindly gets an honest empty result rather than a crash — which matters most in exactly the situation that produces one.
An unknown is a value, never a blank and never a zero
Every measurement arrives as a reading. When nobody has established it, the reading is still there and its value is the literal string “unknown” or “not_published”. Never an omitted key, never null in a numeric field, and above all never 0 — a 0 acre figure or a 0 mile distance reads as “we measured it and there is none”, which is the false negative that ends a real site.
The two literals are different facts and a developer acts on them differently. “unknown” means nobody established it and an import or a survey might; “not_published” means the authority that would know does not publish it, so no amount of our work produces it and the next step is a phone call. Each unknown carries what would resolve it.
The same rule governs change: changes distinguishes “two assessments were compared and agreed” from “the sweep has not reached this site twice”. Collapsing those would let “we have never checked” render as calm.
actionWhich action produced this. The same name on both surfaces, so a log line is greppable.
okWhether an answer was produced. False still carries the whole envelope.
statusok, not_found, invalid or unavailable. This is the field to branch on.
errorA code and one sentence, or null. The sentence is written for a person and is safe to show.
generatedAtWhen this answer was computed — not when anything in it was measured. Measurement dates travel with each reading.
projectThe project this is about, or null.
parcelThe parcel this is about, or null for project-wide actions.
recommendationadvance, review, reject, or none. “none” means the action did not screen anything, not that the answer was neutral.
risksEstablished findings, split into the ones that can end a site and the ones that change cost or design.
unknownsThings nobody has established, stated as values with what would resolve each. Scan these, not the gaps between fields.
nextActionsWhat to do next, and why it is worth doing — each with a `priority` (how urgent) and an `establishment` (how well established the finding behind it is). The two are separate axes and both are on every row; branching on `priority` alone reads a gap nobody has filled, and a withholding, as a confirmed project-ender.
evidenceEvery attributed source with its date, and a confidence verdict over the whole result.
changesWhat moved since the last assessment, or an explicit statement that the sweep has not looked twice. Those are different answers and are never merged.
dataThe action-specific payload, or null.
usageThe meter reading after this call. Null means no reading was taken, never zero.
How well established a value is
Every finding carries a tier and every reading carries a confidence, and both are the same six-value scale, strongest first. This is the vocabulary that decides what a caller is entitled to do with a number — a provisional figure ranks a shortlist and does not support a commitment, and conflicting is weaker than knowing nothing.
These are hyphenated. The tier is “not-published”; a reading’s value for the same fact is “not_published”. They are the same fact in two places, and a switch written against one spelling matches half the payload.
verifiedMay carry a numberThe authority for this fact publishes exactly this fact.
Act on it. This is the only tier that needs no qualification.
impact: The consequence is stated plainly.provisionalMay carry a numberPublished, but by a source whose authority for this fact is unconfirmed — an assessor's personal ArcGIS account, a city layer embedded in a county service.
Usable for ranking, not for a commitment. Name the source when you repeat the number.
impact: The consequence is stated plainly.estimatedMay carry a numberDerived or modelled rather than published: a distance we measured, a capacity proxy. Honest, but ours rather than theirs.
Never present it as published. The impact is conditional on confirming it first, and the finding says so.
impact: The consequence is stated, conditional on confirming the figure first.not-publishedCarries no numberThe authority exists and does not publish this. Distinct from unknown: the answer is unavailable rather than merely not looked for.
No import will produce it. The next step is a request to the authority, and the finding names one.
impact: `impact` is the empty string. That is the tier speaking, not a missing field: an unestablished fact takes less space rather than the same space in softer words.unknownCarries no numberWe looked and could not establish it.
Do not substitute a default. An import or a survey may resolve it, and the finding says which.
impact: `impact` is the empty string. That is the tier speaking, not a missing field: an unestablished fact takes less space rather than the same space in softer words.conflictingCarries no numberTwo or more sources disagree beyond tolerance. Last on purpose: it is a WEAKER basis than knowing nothing, because acting on either number means acting against evidence you already hold.
Never pick one. The record carries a `conflict` array with what each source said, so a reader can see whether the gap is noise or decisive.
impact: The consequence is replaced by an instruction not to rely on either figure.
evidence.confidence is a different field on a different scale — high, medium, low — and it is a verdict over the whole result rather than over one value. Two fields named confidence with different value sets is a trap nobody avoids by accident, so it is named here.
A reading, wherever a measurement appears
Measurements inside data are never bare values. Branch on known.
knownThe field to branch on. `false` means nobody established this, and the reading is still fully present.
valueThe measurement when `known` is true. When it is false this is the literal string “unknown” or “not_published” — never null, never 0, never an omitted key.
unit“acres”, “mi”, “MW”. Present on unknowns too, so you know what is missing.
sourceWho said it, or null. An unattributed reading is not evidence.
asOfWhen the source said it. Always null on an unknown — there is no date on a thing nobody published.
confidenceThe evidence tier, from the six above. This is NOT the same scale as evidence.confidence, which is high/medium/low over the whole result.
whyPresent only when `known` is false: what would establish it. A reading with no route to an answer is a shrug.
A finding, under risks
severityfatal, material or watch. Fatal findings arrive under risks.fatal.
dimensionWhich part of the screen produced it — grid, permitting, land, and so on.
findingWhat was established, in one sentence.
impactWhat it costs, governed by `tier` — see the table above. An empty string is a tier statement, not a missing value.
nextStepThe action that would resolve or price it.
tierOne of the six evidence tiers. Hyphenated: “not-published”, not “not_published”.
sourceThe attributed source, or null.
asOfThe date the source carries, or null.
conflictPresent ONLY at tier “conflicting”: what each source actually said, so the gap can be judged rather than averaged away.
withheldPresent ONLY on the single withheld MARKER, which is not a finding about the land at all: it says this access level was not shown the screening findings. Never count it as a risk, and never render it in the colour of a rejection — it is a fact about the reader.
A step, under nextActions
Two axes, and they are separate: priority says how urgent, establishment says how well established the finding behind it is. A fatal step whose establishment is not established is an open question, not a rejection.
actionThe step itself, in one sentence. Deduplicated across findings — the same step can answer several.
becauseWhat makes it worth doing: the finding it came from, or the headline that produced it.
priorityHOW URGENT, and nothing else: fatal, material, watch, or input. “input” is a question for you rather than work on the land. This is NOT a statement about how well established anything is — see establishment.
establishmentHOW WELL ESTABLISHED the finding behind this step is: “established” (measured, and the tier can carry a conclusion), “unresolved” (nobody has established it), “withheld” (not shown at this access level — a fact about your access, never about the land), or null (no finding produced this step, which is every “input” row and nothing else). A priority of “fatal” with establishment other than “established” is an OPEN QUESTION, not a rejection.
Statuses, and whether to retry
Branch on status in the body rather than on the HTTP code. When an action ran, the two correspond:
200ok400invalid404not_found503unavailable
unavailable never collapses into a 404. A project that could not be read because the store is down has not been shown to be absent, and answering “no such project” during our outage would tell you your pipeline was deleted. An error nobody planned for answers 500 with unavailable in the body — both are 5xx, so retry either way.
Refusals, before any action runs
These are decided by the gate, so nothing was computed and nothing was metered. They still arrive as the same envelope.
401invalidRetrying does not helpNo key was presented, or the key is not one we recognise, or it has been revoked.
Check the Authorization header carries a live key. A revoked key stays revoked; mint a new one from the account panel.
403invalidRetrying does not helpThe key is good and the account is who it says it is, but the plan on that account does not include API access.
The response names the plan. This is a plan question, not a credential one — nothing about the key needs changing.
429unavailableRetrying later helpsThe burst limit: too many calls in an hour on this one key. Counted per key, so one integration's loop cannot stop the same account's others.
Back off and retry. This is almost always a retry running without a backoff rather than genuine volume, and the message says so explicitly to keep it apart from the allowance.
429unavailableRetrying later helpsTwice the included allowance for the billing period. Calls are paused until we have spoken. Nothing has been charged and nothing will be.
The allowance resets on the date in the message. If the volume is real rather than a runaway, this is the conversation about the right allowance.
503unavailableRetrying later helpsWe could not check the key, read the account, or read usage. This is our outage and says nothing about the credential — a key that could not be checked has not been shown to be bad.
Retry. Do not treat this as an invalid key and do not mint a replacement.
No Retry-After header accompanies either 429. The limiter falls back to a per-instance rolling window when the shared counter is unreachable, so the remaining time is not a figure we always have — and a wrong Retry-After is worse than none, because a client trusts it and comes back to another refusal.
How calls are counted
One action call is one unit. Not rows, not tokens, not requests. One screening call is one unit whether it screened two sites or two hundred. It is the only unit you can predict before you call, and it does not move when our implementation gets faster or slower.
A call is counted once the action produced an answer, whatever that answer says — a not_found is real work and is counted. A refusal by the gate is never counted, and neither is a request whose arguments were rejected before anything ran. Every metered response carries a usage field with the reading after that call; null there means no reading was taken, not zero.
withinfrom 0% of what is includedServedInside the included allowance. The notice says where you stand and when it resets.
warningfrom 80% of what is includedServedThe warning, with enough allowance left to finish what you are doing. Going over costs nothing.
over-includedfrom 100% of what is includedServedPast the included amount and STILL SERVED. An agent halfway through a workflow does not stop politely, so this is a conversation rather than a wall — and not a charge.
refusedfrom 200% of what is includedPausedTwice the included amount. Calls are paused until we have spoken. This bound exists because a looping caller can spend a period's allowance in an afternoon and there is no invoice at the end to make anyone notice.
There is never an automatic charge. Not at the warning, not past the included amount, not at the pause. Going over is a conversation about the right allowance, never an invoice, and every usage notice says so in its own words.
A separate per-hour ceiling applies per key rather than per account, so a retry loop in one integration cannot take down the same account’s nightly job. It is a safety interlock and not a quota: its refusal names the rate and never speaks in the allowance’s words, because somebody with a retry bug must not be sent to buy a bigger plan.
MCP
The same tools, the same envelopes and the same meter, over JSON-RPC at a single endpoint:
POST /api/agent/mcp
The methods implemented are initialize, tools/list, tools/call, ping. tools/list is discovery — it returns the names, descriptions and JSON Schemas above — and it is not metered. Only tools/call is counted. Every method is still authenticated and still passes through the per-hour ceiling, because a loop calling tools/list is the same runaway as a loop calling anything else.
The server is stateless: no session id is issued and none is read. Every request carries its own key and is authenticated, limited and metered on its own. Batched arrays are refused — one HTTP request is one authentication and one limit decision, so a 500-item array would be 500 action calls behind a single check.
A protocol failure — bad JSON, an unknown method, a missing tool name — is a JSON-RPC error. A tool failure, where the action ran and answered not_found or invalid, is a result with isError set, carrying the whole envelope. That split is what lets a caller tell “I called this wrong” from “the answer is that there is no such project”.
The endpoint list on this page is checked against the route files themselves, and every description is imported from the tool definition both surfaces share, so this page cannot document an endpoint that does not exist or describe one differently from the schema your agent reads. GridCite is preliminary GIS screening: nothing returned by any endpoint here is a survey, a title review, an interconnection study or a permit decision — see the Site-Screening Disclaimer.