Skip to main content
A Data Feeds subscription is the set of datasets Ploti delivers to your own systems and keeps current. It covers every deliverable dataset in the places you name, optionally limited to some topics, plus any datasets you name on their own. It belongs to your workspace (your team’s Ploti account), not to the person who created it. Datasets Ploti adds to its places later join it on their own.

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 state place 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 for us) and how many datasets it holds, in all (datasetCount) and by topic (datasetCounts).
  • Pass deliverable=true to count only the datasets Data Feeds delivers.
  • It returns every place at once, filtered by state or kind if you pass them.
  • Use the ids exactly as it gives them.
A statewide dataset is in your coverage when a county of its state is, and a national one with any coverage.

Topics

A dataset’s topic 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 from datasets 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 by place, topic, subscription or updatedSince.
  • 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.
  • dataAtlasId opens the dataset in Ploti’s app: https://app.ploti.ai/map?dataAtlasDatasetId= with it shows the dataset on the map.
  • place.kind is county, state or us. description, place.id, place.name, topic, source.publisher and source.url can be null.
  • keyed and keyColumns are those of the version in current, so a key a newer version brings shows once that version is released.
  • current: the newest released version, or null until the first release. current.publishedAt is when that version was released to subscribers, not when it was built, and the updatedSince filter on GET /v1/feeds/datasets compares against it. A dataset’s files are built only once a subscription includes it, so a dataset you have just subscribed to can be null until its first version is built and sent.
  • status: available when current is set. Otherwise why not: not_built until a subscription includes the dataset, then building until its first version is sent; held or failed while its first version waits on Ploti’s staff; or awaiting_release when 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 moves lastCheckedAt. A day past nextCheckBy, the dataset is stale and you receive feed.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 sends feed.dataset.added. fl-counties names 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, within places. Every topic when left out. A dataset’s topic is its primary topic, the topic field on the dataset.
  • datasetIds: datasets to include on their own, such as statewide ones, from anywhere in your coverage.
Three rules apply:
  • 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.
Every field is checked when you create or change a subscription: 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:
  • total is exact: a dataset over several of the places you name counts once. It includes datasets with no topic, which a place’s datasetCounts in GET /v1/places leaves out, so the two can differ by those.
  • byTopic and byPlace count the same datasets, by primary topic (null for none) and by the place each is filed under.
  • datasets is one page. Narrow it with q (title search), place, topic (none for no topic), matchedBy and sort (place, title or updated). matching says how many are left after them.
A preview is refused with:
  • 400 invalid_input for an empty or malformed body
  • 409 outside_coverage for a place outside your coverage
  • 409 not_deliverable for a dataset Ploti cannot deliver to you
  • 503 plan_unavailable while Ploti cannot read your plan
The 25-subscription limit does not apply to a preview. A preview counts toward the stricter limit of 20 calls a minute.

Changing a subscription

Change a subscription with PATCH /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: 400 invalid_input
  • a stale one: 412 precondition_failed. Read the subscription again and resend.
  • a change that would leave no places and no datasets: 400 invalid_input. Delete the subscription to stop receiving.
Datasets that join send 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 answers 409 not_subscribed instead.
Links already issued keep working until they expire, up to an hour. Files you already downloaded are yours.

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:
  1. It brings the subscription’s datasets up to date. A dataset the coverage or plan no longer opens leaves with a feed.dataset.removed event before the manifest’s cursor.
  2. It lists the datasets the subscription delivers as of its eventCursor that 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 after eventCursor still apply.
  • No released version yet: {"datasetId", "reason"}, with its status as the reason: building, held, failed or awaiting_release (see datasets). Its first version arrives as feed.dataset.resync_required with reason first_version.
When Ploti cannot read your workspace’s plan just now, the manifest answers 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:
  • 410 expired once 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 retention
  • 400 invalid_input if it is past the subscription’s newest event
Without 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}/links signs 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:PutObject on 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 a current.json written last.
Tell us if you need push delivery, and where you ingest, so we know which destination to build first.

Format

Files are GeoParquet: geometry as WKB in EPSG:4326, readable by warehouses, DuckDB, pandas and Spark. Each version’s schema.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 as datasets search. Over a limit, the answer is 429 rate_limited with a Retry-After header: wait that many seconds.