Places
A subscription is built from places and topics, the same ones Find datasets describes. A place is where a dataset applies:
A city or region dataset is filed under its county.
A dataset that covers several counties counts toward each one’s
datasetCounts, is listed under each with GET /v1/feeds/datasets?place=, and joins a subscription that names any of them. Its place.id and its rows’ _ploti_place_id still name one county: the county the dataset is filed under or, when it is filed under none, the first county it covers.
A <state>-counties place is every county your coverage has in that state, as one place.
- A subscription that names it picks up a county added to your coverage later, and drops one taken out.
- It does not include the state’s statewide records. Name the
stateplace for those. - A dataset over several of its counties counts once toward it.
GET /v1/places lists the places in your workspace’s coverage: its counties, their states, each state’s counties and us.
- Each place comes with its
state(null forus) and how many datasets it holds, in all (datasetCount) and by topic (datasetCounts). - Pass
deliverable=trueto count only the datasets Data Feeds delivers. - It returns every place at once, filtered by
stateorkindif you pass them. - Use the ids exactly as it gives them.
Topics
A dataset’stopic is its primary topic, from a fixed list such as parcels-property, zoning-land-use or development-activity (permits, plans and inspections), or null when it has none. GET /v1/topics returns the list. The topic filter on GET /v1/feeds/datasets, a subscription’s topics and a place’s datasetCounts all use the same primary topic.
Datasets
A dataset is one set of public records from one source, such as Orange County’s building permits. Dataset ids are the same ones Ploti’s other APIs and agents use, so an id fromdatasets search works here too.
GET /v1/feeds/datasets(ploti feeds datasets list) lists the datasets your subscriptions could deliver, up to 200 a page. Narrow it byplace,topic,subscriptionorupdatedSince.GET /v1/feeds/datasets/{dataset}(ploti feeds datasets get) returns one.
- Only deliverable datasets are listed: datasets Ploti’s agents collect that your workspace’s coverage and plan open.
dataAtlasIdopens the dataset in Ploti’s app:https://app.ploti.ai/map?dataAtlasDatasetId=with it shows the dataset on the map.place.kindiscounty,stateorus.description,place.id,place.name,topic,source.publisherandsource.urlcan be null.keyedandkeyColumnsare those of the version incurrent, so a key a newer version brings shows once that version is released.current: the newest released version, ornulluntil the first release.current.publishedAtis when that version was released to subscribers, not when it was built, and theupdatedSincefilter onGET /v1/feeds/datasetscompares against it. A dataset’s files are built only once a subscription includes it, so a dataset you have just subscribed to can benulluntil its first version is built and sent.status:availablewhencurrentis set. Otherwise why not:not_builtuntil a subscription includes the dataset, thenbuildinguntil its first version is sent;heldorfailedwhile its first version waits on Ploti’s staff; orawaiting_releasewhen Ploti has nothing to build it from until its source is released again.refresh: when Ploti last checked the source and when the next check is due. Ploti checks a source on schedule while a subscription includes its dataset, so for other datasets these times can be old. A check that finds nothing new publishes no version but still moveslastCheckedAt. A day pastnextCheckBy, the dataset is stale and you receivefeed.dataset.stale.
What a subscription covers
places: every deliverable dataset in each place, now and later. A dataset Ploti adds to one of them joins the subscription and sendsfeed.dataset.added.fl-countiesnames every county your coverage has in Florida as one place, so a subscription to a whole state fits within the 500 places a subscription may name, and picks up counties added to your coverage later.topics: only datasets whose topic is one of these, withinplaces. Every topic when left out. A dataset’s topic is its primary topic, thetopicfield on the dataset.datasetIds: datasets to include on their own, such as statewide ones, from anywhere in your coverage.
- A subscription names at least one place or dataset.
- A workspace may have at most 25 subscriptions at a time. Deleting one makes room for another.
- Any member can read the workspace’s subscriptions and what they deliver. Creating, changing and deleting one takes a workspace admin.
A create takes an
Idempotency-Key header. A retry with the same key within a day returns the first answer instead of creating twice, with an Idempotent-Replayed: true header. The first answer carries the ETag header. A replay does not, but its body always has revision.
Preview before you subscribe
POST /v1/feeds/subscriptions/preview takes the same places, topics and datasetIds and answers what a subscription with them would deliver now, without creating one:
totalis exact: a dataset over several of the places you name counts once. It includes datasets with no topic, which a place’sdatasetCountsinGET /v1/placesleaves out, so the two can differ by those.byTopicandbyPlacecount the same datasets, by primary topic (nullfor none) and by the place each is filed under.datasetsis one page. Narrow it withq(title search),place,topic(nonefor no topic),matchedByandsort(place,titleorupdated).matchingsays how many are left after them.
400invalid_inputfor an empty or malformed body409outside_coveragefor a place outside your coverage409not_deliverablefor a dataset Ploti cannot deliver to you503plan_unavailablewhile Ploti cannot read your plan
Changing a subscription
Change a subscription withPATCH /v1/feeds/subscriptions/{subscriptionId}. Fields you send replace those fields, and fields you leave out are kept. null is not accepted: send [] to clear a list.
Every subscription has a revision, and reads return it as the ETag header ("3"). Send it back as the If-Match header, or as revision in the body, so two edits never overwrite each other. When both are sent, the body’s revision is used.
- neither:
400invalid_input - a stale one:
412precondition_failed. Read the subscription again and resend. - a change that would leave no places and no datasets:
400invalid_input. Delete the subscription to stop receiving.
feed.dataset.added; datasets that leave send feed.dataset.removed. Rows you already loaded from a removed dataset stay yours, and stop updating.
DELETE /v1/feeds/subscriptions/{subscriptionId} answers 200 with {"deleted": true}. While the subscription’s datasets are changing, it answers 409 busy: try again in a moment. Deleting a subscription:
- stops its events and its webhooks
- stops
GET /v1/feeds/datasets/{dataset}/versions/{version}signing links for its datasets, unless another of the workspace’s subscriptions delivers them. It answers409not_subscribedinstead.
The manifest
GET /v1/feeds/subscriptions/{subscriptionId}/manifest is the subscription’s list of datasets, with links to load each one. Without asOf, it does two things:
- It brings the subscription’s datasets up to date. A dataset the coverage or plan no longer opens leaves with a
feed.dataset.removedevent before the manifest’s cursor. - It lists the datasets the subscription delivers as of its
eventCursorthat have a released version. Each comes with that version and signed links to its snapshot, schema and attachments, the files Ploti holds for the dataset.
eventCursor is the last event already reflected in those snapshots: load them, then read events after it. Each dataset’s keyed is that of the version listed.
A dataset is listed only while the subscription still delivers it and your workspace’s coverage and plan still open it. Two kinds of dataset are listed under unavailable instead:
- Files being rebuilt:
{"datasetId", "version", "reason": "rebuilding"}. Ask for the manifest again in a few minutes to load it. Its events aftereventCursorstill apply. - No released version yet:
{"datasetId", "reason"}, with itsstatusas the reason:building,held,failedorawaiting_release(see datasets). Its first version arrives asfeed.dataset.resync_requiredwith reasonfirst_version.
503 plan_unavailable: try again in a minute.
Pass asOf with an eventCursor from an earlier manifest to rebuild the datasets and versions as of that event, with fresh links. It is for refreshing links to a recent cursor, such as one from a load that is still running. It lists only datasets the subscription still delivers, so it never returns links to a dataset removed since.
asOf is refused with:
410expiredonce any event up to it has been pruned, when a version it reached has since been released again and rebuilt, or when a listed version’s snapshot is past retention400invalid_inputif it is past the subscription’s newest event
asOf, the manifest does not answer 410. Events are kept for 90 days, so once a subscription is older than that, its first events are pruned and every asOf answers 410 expired.
To refresh links to a version you are loading, use one of these:
GET /v1/feeds/datasets/{dataset}/versions/{version}signs fresh links to one version, for a dataset one of your subscriptions delivers.POST /v1/feeds/datasets/{dataset}/versions/{version}/linkssigns links for only the files you name.
Getting the files
You fetch files yourself with signed links from the manifest and from each version. Links last one hour. Ask again for fresh ones at any time while a subscription delivers the dataset. This is how Data Feeds delivers today.Push to your storage (planned)
Writing each version’s files into your own storage is planned and not yet built. The plan, starting with Amazon S3:- You create a role Ploti can assume, with
s3:PutObjecton your bucket and prefix. - Ploti gives each subscription its own
ExternalId, which your role’s trust policy must require, so another Ploti customer cannot name your role. - Ploti writes each version’s files under
{prefix}{datasetId}/versions/{version}/, with acurrent.jsonwritten last.
Format
Files are GeoParquet: geometry as WKB in EPSG:4326, readable by warehouses, DuckDB, pandas and Spark. Each version’sschema.json and manifest.json are JSON. Attachments are in whatever format Ploti stored them, named by contentType.
Rate limits
Each API key may make 120 calls a minute. Creating, changing or previewing a subscription and getting a manifest also count toward a stricter limit of 20 a minute, shared with Ploti’s other calls that search widely or start work, such asdatasets search. Over a limit, the answer is 429 rate_limited with a Retry-After header: wait that many seconds.