PermitIntel.aiPermit Data Intelligence
Menu

Supplier Portal · Statewide

Data delivery

Your territory's accounts, permits and market rollups, delivered as CSV or Parquet to your own SFTP server, Amazon S3 bucket or Google Cloud Storage bucket once a day or once a week, or pulled from a signed URL, or added to ArcGIS Online as a live layer. Set it up under Settings → Delivery.

Datasets

Every file is built fresh at delivery time and bounded by the counties your plan covers, re-checked on every run.

DatasetOne row perWindow and sizeLoad key
accountsContractor business in your territory with a permit in the last 12 months: licenses, trade, 12-month and 90-day activity, value, momentum, grade, best phone and email, workers' comp carrierFull snapshot, most active first, up to 25,000 rows per filebusiness_group_id + snapshot_date
permitsPermit issued in your territory: number, jurisdiction, dates, status, work, declared value, site address, parcel, contractor and licenseIssue date in the last 3 days (daily) or 8 days (weekly), up to 20,000 rows per file. Windows overlap on purpose.permit_id
market_rollupsCounty × product category × month: permit count, declared value, residential / commercial split, permits attributed to a licensed contractorLast 36 months; the current month is flagged month_is_partialcounty_fips + category + month

Parquet files carry typed columns (dates as DATE, counts as INT32, dollars as DOUBLE) and key/value metadata (permitintel.snapshot_date, permitintel.territory, permitintel.rows, permitintel.capped). CSV files use the same column names in the same order. Permit data reflects the records PermitIntel has ingested and is not represented as a complete record of all permits.

Push to your SFTP server, S3 bucket or Cloud Storage bucket

You bring the destination and the credentials; we never deliver into storage of ours. A target runs once a day (daily) or once a week (weekly). Test connection writes a small file named .permitintel_connection_test.txt where deliveries will land. Run now delivers immediately through the same code path as the schedule.

Where files land

SFTP:           <path>/permitintel_<dataset>_<YYYY-MM-DD>.<csv|parquet>
S3 / GCS:       <prefix><dataset>/dt=<YYYY-MM-DD>/permitintel_<dataset>_<YYYY-MM-DD>.<csv|parquet>

example:        s3://acme-data/permitintel/accounts/dt=2026-09-23/permitintel_accounts_2026-09-23.parquet

SFTP

Host, port, username, and a password or a private key (OpenSSH or PEM, optional passphrase). Paste the server's SHA256 host key fingerprint to pin it; Test connection shows the one it saw. Files are written as .part and renamed when complete.

Amazon S3

Bucket, region, optional prefix, and an access key for an IAM user that can s3:PutObject on arn:aws:s3:::your-bucket/your-prefix/*. Nothing else is needed. S3-compatible stores work with a custom endpoint.

Google Cloud Storage

Bucket, optional prefix, and an HMAC key (Cloud Storage → Settings → Interoperability) for a service account with Storage Object Creator on the bucket. Re-running a delivery on the same day overwrites that day's file, which Cloud Storage only allows with delete permission too: grant Storage Object User if you use Run now more than once a day.

Prefixes may use letters, digits and / _ . = -.

Signed file feeds

A file feedtarget gives you one URL that returns the dataset, built fresh, as Parquet or CSV. The token in the URL is shown once, when you create the feed; we store only a hash of it. Rotate it or turn the feed off from Settings → Delivery. A feed builds at most once a minute; a faster poll gets 429 with Retry-After: 60.

curl -fo permitintel_accounts.parquet \
  "https://permitintel.ai/api/delivery/feed/accounts.parquet?token=pif_your_feed_token"

# response headers tell you what you got
X-Records: 5138
X-Snapshot-Date: 2026-09-23
X-Window: 2026-09-21..2026-09-23      # permits only

Treat a feed URL like a password: anyone holding it can download the file. Like any URL, it is recorded in our hosting provider's request logs; we never write it into our own logs or error messages.

Snowflake

Snowflake loads from the S3 or Cloud Storage bucket we deliver into. Create a storage integration for your bucket (your Snowflake admin does this once, following Snowflake's own guide for S3 or GCS), then a stage and a COPY INTO. COPY INTOremembers which files it has loaded, so running it after every delivery loads only the new day's file.

CREATE FILE FORMAT permitintel_parquet TYPE = PARQUET;

CREATE STAGE permitintel_stage
  URL = 's3://acme-data/permitintel/'          -- or 'gcs://acme-data/permitintel/'
  STORAGE_INTEGRATION = acme_s3_integration
  FILE_FORMAT = permitintel_parquet;

-- one-time: a table shaped like the files
CREATE TABLE permitintel_accounts USING TEMPLATE (
  SELECT ARRAY_AGG(OBJECT_CONSTRUCT(*))
  FROM TABLE(INFER_SCHEMA(
    LOCATION => '@permitintel_stage/accounts/',
    FILE_FORMAT => 'permitintel_parquet')));

-- after each delivery (or on a Snowflake task)
COPY INTO permitintel_accounts
  FROM @permitintel_stage/accounts/
  FILE_FORMAT = (FORMAT_NAME = 'permitintel_parquet')
  MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;

-- accounts files are daily snapshots: read the latest one
SELECT * FROM permitintel_accounts
WHERE snapshot_date = (SELECT MAX(snapshot_date) FROM permitintel_accounts);

Permits windows overlap by design. Load them into a staging table the same way, then MERGE INTO permits USING permits_stage ON permits.permit_id = permits_stage.permit_id.

No bucket? Pull the signed feed and load it through your user stage with SnowSQL:

curl -fo /tmp/permitintel_accounts.parquet "https://permitintel.ai/api/delivery/feed/accounts.parquet?token=pif_..."
snowsql -q "PUT file:///tmp/permitintel_accounts.parquet @~/permitintel/ AUTO_COMPRESS=FALSE;
            COPY INTO permitintel_accounts FROM @~/permitintel/
              FILE_FORMAT = (TYPE = PARQUET) MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;"

BigQuery

BigQuery reads Parquet straight from the Cloud Storage bucket we deliver into. Load one day:

bq load --source_format=PARQUET --replace \
  mydataset.permitintel_accounts \
  "gs://acme-data/permitintel/accounts/dt=2026-09-23/permitintel_accounts_2026-09-23.parquet"

Or define an external table over every delivery, with the dt= folders as a DATE partition column:

CREATE EXTERNAL TABLE mydataset.permitintel_accounts_all
WITH PARTITION COLUMNS (dt DATE)
OPTIONS (
  format = 'PARQUET',
  uris = ['gs://acme-data/permitintel/accounts/*'],
  hive_partition_uri_prefix = 'gs://acme-data/permitintel/accounts'
);

SELECT business_name, county, permits_12mo, phone
FROM mydataset.permitintel_accounts_all
WHERE dt = (SELECT MAX(dt) FROM mydataset.permitintel_accounts_all)
ORDER BY permits_12mo DESC;

Parquet DATE columns arrive as DATE, INT32 as INTEGER and DOUBLE as FLOAT. The HMAC key we write with and the identity BigQuery reads with are both yours.

Databricks

From the S3 or Cloud Storage path we deliver into (your workspace needs read access to it, for example through an external location):

-- look at the files
SELECT * FROM read_files('s3://acme-data/permitintel/accounts/', format => 'parquet');

-- load incrementally: COPY INTO skips files it has already loaded
CREATE TABLE IF NOT EXISTS permitintel_accounts;
COPY INTO permitintel_accounts
  FROM 's3://acme-data/permitintel/accounts/'
  FILEFORMAT = PARQUET
  COPY_OPTIONS ('mergeSchema' = 'true');

From the signed feed, in a notebook:

import pandas as pd

pdf = pd.read_parquet("https://permitintel.ai/api/delivery/feed/accounts.parquet?token=pif_...")
spark.createDataFrame(pdf).write.mode("overwrite").saveAsTable("permitintel_accounts")

ArcGIS Online, ArcGIS Pro and QGIS

A GeoJSON layertarget gives you a URL that returns your territory's accounts as points (the business's licensed address, geocoded by the US Census geocoder, or the center of the parcel at that address when Census cannot place it; each point says which in geo_source). Businesses we cannot place are counted in unplaced and left off the map.

  1. In ArcGIS Online, open Map Viewer, choose Add → Add layer from URL.
  2. Paste the URL from Settings → Delivery. ArcGIS recognizes it as GeoJSON.
  3. Style by primary_trade or size by permits_12mo; pop-ups show name, license, phone and activity.
https://permitintel.ai/api/v1/geo/accounts.geojson?token=pif_your_layer_token&limit=5000
https://permitintel.ai/api/v1/geo/accounts.geojson?token=pif_your_layer_token&territory=Broward&limit=5000

The token is in the URL because ArcGIS Online cannot send a header when it adds a layer by URL. Anyone with the URL sees the layer; rotate the token in Settings if it is shared by mistake.

ArcGIS Online reads the one URL you give it, and a page holds at most 5,000 accounts, most active first. For a larger territory add one layer per county with territory=. Scripts can walk every page: each response carries numberMatched, numberReturned, exceededTransferLimit and a next link, and accepts limit/offset or ArcGIS's resultRecordCount/resultOffset.

import requests

url = "https://permitintel.ai/api/v1/geo/accounts.geojson?token=pif_...&limit=5000"
features = []
while url:
    page = requests.get(url, timeout=120).json()
    features += page["features"]
    url = next((l["href"] for l in page["links"] if l["rel"] == "next"), None)

Security

  • Credentials are encrypted with AES-256-GCM before they are stored, with a key held outside the database. They are never shown again after you save them, including to you; leave a secret field blank when editing to keep it.
  • Errors from your server or bucket are stored and shown with credentials removed.
  • Feed and layer tokens are stored only as a SHA-256 hash, can be rotated at any time, and each one opens a single target.
  • Only the workspace owner can add, edit, test, run or rotate a delivery target.
  • Deliveries go to public hosts only; private and internal network addresses are refused.