Baltimore City Permits & Property Data
Pricing
from $0.03 / 1,000 baltimore permit or property records
Baltimore City Permits & Property Data
Baltimore City building permits (public data, 292,819 permits) and real-property records (238,148 parcels) in one actor, linked by block/lot: who pulled a permit for what and where, plus owner, sale price, year built, assessed values and zoning for the parcel.
Pricing
from $0.03 / 1,000 baltimore permit or property records
Rating
0.0
(0)
Developer
Wenhao Yang
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 hours ago
Last modified
Categories
Share
Low cost
From $0.00005 per record, down to $0.00003 at Gold - pay per record delivered, and nothing for the query. Each record costs a fraction of a cent.
Two Baltimore City datasets over the same parcels: what the city has permitted, and what sits on the land.
What you get
| Corpus | Rows | What it holds |
|---|---|---|
| permits (default) | 292,819 | building permits: case number, description, issue / expiration dates, address, block/lot, existing and proposed use, council district, neighbourhood, cost, permit name |
| property | 238,148 | the real-property roll: PIN and block/lot, up to three owner names, property and mailing address, deed book/page, sale date and price, year built, structure area, land / improvement / full-cash values with exemptions, use group, zoning code, neighbourhood, vacant flag |
Why two corpora is the point
Both tables carry the city's block/lot parcel key, and both are populated on 100% of rows. That turns two datasets into one answer - in two calls: this lot is zoned R-6, has this owner, last sold for this much, was built in this year, and here are the fourteen permits pulled on it.
Neither dataset alone tells you that. The permit record knows the work but not the owner or the value; the roll knows the parcel but not what was done to it. Both sides carry the blocklot key, so you join them yourself - the permit records do not repeat the owner or the value, and the roll does not carry the permits.
Modes
- permits (default) - newest permits first
- property - parcels, highest sale price first
- aggregate - one count row per group: by neighbourhood, use, district (permits) or use group, zoning, vacancy (property)
Filter permits by case number, address, neighbourhood, council district, existing/proposed use, permit name or issue date. Filter property by owner, address, neighbourhood, ZIP, zoning code, use group, vacancy, year built, sale price or sale date. A filter that belongs to the other corpus is rejected rather than quietly ignored.
Example inputs
One parcel's property record - blocklot is the city's parcel key, and it
works in both corpora.
{ "corpus": "property", "blocklot": "3881 030" }
Every permit filed on that same lot - switch the corpus, keep the key.
{ "corpus": "permits", "blocklot": "3881 030", "maxResults": 50 }
Buildings the city has flagged vacant - a Y/N filter selects only the
flagged subset, not every vacant building; the parcels with no flag at all form
their own unrecorded bucket in a vacant rollup rather than being counted as
vacant.
{ "corpus": "property", "vacant": "Y", "maxResults": 100 }
A neighbourhood's zoning mix - one count per group over the parcels.
{ "corpus": "property", "neighborhood": "Fells Point","aggregate": true, "groupBy": "zoneCode" }
Why this is hard
Two dates in this data are strings, and the format is not the one you would guess. Sale date and layer-update date arrive as MMDDYYYY - 09202026 means 20 September 2026, and reads as a nonsense year if you parse it as anything else.
The vacant flag is mostly absent, and that matters. About 209,000 of the 238,148 parcels carry no flag at all - neither Y nor N, and the aggregate reports that bucket as unrecorded rather than leaving it blank. Filtering for vacant buildings gives you the 11,371 that are flagged vacant, not "every vacant building", and a product that blurred the two would be quietly wrong.
Zoning codes are padded with trailing blanks. R-8 is stored as R-8 . The good news is that equality already ignores the padding, so R-8 finds all 71,951 rows - but anything that tries to normalise the value first (trimming, concatenating, prefixing) fails against this service and takes the filter down with it.
And the parcel tail is not representative. 7,290 parcels carry no address at all, and the most recently loaded rows are where they cluster - including 137 PSC* records that are entirely blank. Ordering the default by load order opens on rows with nothing to show, so the parcel view is ordered by sale price instead, which opens on records that actually carry data - though a handful of blank-address rows survive even there, since a sale price is not an address.
Zero is not a year and not a price. 39,847 parcels have no year built recorded and 68,682 have never sold, and both are stored as 0 rather than blank. A naive "built before 1900" would return 67,674 rows, two thirds of them buildings whose age nobody knows; a naive price ceiling would sweep in every never-sold parcel. Any bound here excludes the zeros, and the fields come back empty rather than as a literal zero.
A note on what is not here
This actor covers permits and property only. Baltimore does publish citations - the Environmental Control Board violation ledger (153k rows, with fine amounts and balances) - and we ship it as a separate actor, md-balt-ecb-citations, rather than folding it in here.
Worth flagging one common mix-up: a large "blight tickets" dataset is often mistaken for Baltimore's. It is Detroit's, filed under a Detroit agency, and is not used anywhere in this actor.
Output
One schema across both corpora: every record carries the full field set, empty where that corpus has nothing to say, so the same downstream code reads either. Dates arrive as YYYY-MM-DD, and each record carries sourceUpdatedAt, the layer's own last-refresh date.
Example output
One parcel - corpus=property, blocklot=3881 030 returns that parcel's
roll record (the permit fields come back empty, because the roll does not carry
them). Swap corpus to permits and the same key returns the permits filed on
the lot. Every record carries platform, source, and the aggregate columns
groupKey / groupCount / groupBy, which are "" outside aggregate mode:
{"platform": "md-baltimore-permits-property","source": "baltimore-city-open-data","groupKey": "", "groupCount": "", "groupBy": "","address": "3119 BARCLAY ST","blocklot": "3881 030","caseNumber": "","corpus": "property","cost": "","councilDistrict": "","deedBook": "LGA18350","deedPage": "0317","description": "","dwellingUnits": "1","exemptImprovement": "0","exemptLand": "0","existingUse": "","expirationDate": "","fullCashValue": "0","improvementValue": "9500","issuedDate": "","landValue": "60000","lotSize": "21X110","mailingAddress": "P.O. BOX 16013, 21218","neighborhood": "ABELL","owner1": "BARCLAY, LLC","owner2": "","owner3": "","permitName": "","pin": "3881030","proposedUse": "","saleDate": "2016-08-08","salePrice": "45025","sourceUpdatedAt": "2026-09-27","structureArea": "1120","useGroup": "R","vacant": "Y","yearBuilt": "1900","zipCode": "21218","zoneCode": "R-7"}
Notes
- Public open data from Baltimore City's open GIS. No login, no scraping.
- Charges are metered per record delivered, so a parcel lookup costs a fraction of a cent.