Permit Scouter API
Building permits from Tampa Bay, Florida, collected every morning and cleaned into one format. Every paid call is settled with x402: no account and no API key, just a wallet with USDC on Base.
Getting started
The API lives at https://api.permitscouter.com. All responses are JSON. Try a free route first:
curl https://api.permitscouter.com/sample
An interactive reference with every parameter is at /docs on the API.
Paying with x402
Call a paid route without paying and you get 402 Payment Required. The PAYMENT-REQUIRED header holds the price, the network and the address to pay. Your client signs a USDC authorization for that amount and repeats the request with it in the PAYMENT-SIGNATURE header. The response's PAYMENT-RESPONSE header holds the settlement receipt, including the transaction hash.
You don't need ETH for gas: the payment is settled for you. Any x402 client works. In Python:
pip install "x402[httpx,evm]"
import asyncio
from eth_account import Account
from x402 import x402Client
from x402.http.clients import x402HttpxClient
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.mechanisms.evm.signers import EthAccountSigner
async def main():
client = x402Client()
register_exact_evm_client(client, EthAccountSigner(Account.from_key("0xYOUR_PRIVATE_KEY")))
async with x402HttpxClient(client) as http:
response = await http.get(
"https://api.permitscouter.com/permits/search",
params={"trade": "roofing", "since": "2026-10-01", "min_value": 10000},
)
print(response.json())
asyncio.run(main())
Keep your agent's spending wallet separate from any wallet holding savings, and fund it with only what it needs.
GET /permits/search$0.02New permits, newest first. Within each day, permits with a job value and contractor come first. Up to 25 per call; page with offset.
| Parameter | What it does |
|---|---|
trade | One of roofing, hvac, solar, pool, electrical, plumbing, windows_doors, new_construction, remodel, demolition |
since, until | Filed on or after / on or before a date, YYYY-MM-DD |
min_value | Minimum job value in US dollars |
zip, city | Limit to a zip code or city |
jurisdiction | A source key from /coverage, such as tampa or pinellas |
q | Words in the work description, such as heat pump |
include_applications | true to also return applications not yet issued a permit number (off by default) |
limit, offset | Page size (1 to 25) and starting point |
{
"total": 21,
"next_offset": 5,
"results": [
{
"permit_key": "tampa:BTR-27-0633248",
"jurisdiction": "City of Tampa",
"permit_number": "BTR-27-0633248",
"record_type": "Residential Roof Trade Permit",
"status": "Issued",
"description": "re-roof",
"address": "4202 W CLEVELAND ST",
"city": "TAMPA",
"zip": "33609",
"applied_date": "2026-10-10",
"valuation": 16998.05,
"contractor_name": "EXCEL EXTERIORS, INC.",
"contractor_license": "CCC1331677",
"contractor_key": "ccc1331677",
"trades": ["roofing"],
"source_url": "https://aca-prod.accela.com/TAMPA/…",
"first_seen": "2026-10-11T00:06:09+00:00",
"last_changed": "2026-10-11T00:07:40+00:00"
}
]
}
GET /permits/{source}/{permit_number}$0.01One permit and every change we've recorded to its status, value or issue date, oldest first. Example: /permits/tampa/BTR-27-0633248.
GET /properties/history?address=…&zip=…$0.05Every permit at one street address, newest first. Use the address as it appears in results, such as 4202 W CLEVELAND ST. Adding zip narrows the match.
GET /contractors/{contractor_key}/history$0.10Every permit a contractor has pulled in our coverage area, with the total job value, first and latest permit dates, and a count by trade. The contractor_key is in every permit record; it's usually the state license number in lower case.
MCP tools
The same data is available as MCP tools, for AI assistants and agent frameworks. Connect over streamable HTTP to https://api.permitscouter.com/mcp. No sign-up is needed.
| Tool | What it returns | Per call |
|---|---|---|
coverage | Cities covered, permit counts and last update time | Free |
sample_permits | Three recent permits, to see the record format | Free |
search_permits | The same search as GET /permits/search | $0.02 |
get_permit | One permit with its recorded history | $0.01 |
property_history | Every permit at one address | $0.05 |
contractor_history | A contractor's full permit record | $0.10 |
The free tools work in any MCP client. A paid tool called without payment returns an error result whose text is the x402 payment request; an x402-aware MCP client pays it and calls again with the payment in _meta["x402/payment"]. The receipt comes back in _meta["x402/payment-response"], and a call that fails is not charged.
In Python, with the official MCP library and x402's MCP client, payment is automatic:
pip install "x402[mcp,evm]" "mcp>=1.20,<2"
import asyncio
from datetime import timedelta
from eth_account import Account
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from x402 import x402Client
from x402.mcp import wrap_mcp_client_with_payment
from x402.mechanisms.evm.exact import register_exact_evm_client
from x402.mechanisms.evm.signers import EthAccountSigner
class SessionAdapter:
"""x402's MCP client passes one params dict; the MCP session takes the parts separately."""
def __init__(self, session):
self.session = session
async def call_tool(self, params, read_timeout_seconds=None, **_):
if isinstance(read_timeout_seconds, (int, float)):
read_timeout_seconds = timedelta(seconds=read_timeout_seconds)
return await self.session.call_tool(params["name"], params.get("arguments"),
read_timeout_seconds=read_timeout_seconds,
meta=params.get("_meta"))
async def main():
payments = x402Client()
register_exact_evm_client(payments, EthAccountSigner(Account.from_key("0xYOUR_PRIVATE_KEY")))
async with streamablehttp_client("https://api.permitscouter.com/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
paid = wrap_mcp_client_with_payment(SessionAdapter(session), payments, auto_payment=True)
result = await paid.call_tool("search_permits", {"trade": "roofing", "min_value": 10000})
print(result.content)
asyncio.run(main())
Free routes
GET /coverageFreeThe cities covered today, permit counts, date range, and when each was last updated. Check it before relying on a city.
GET /sampleFreeThree recent permits, so you can see the record format before paying.
GET /healthFreestatus says whether the API is up; collection says whether the data is fresh, with any problems listed.
The permit record
- Freshness. Each source is collected every morning and the last three days are re-checked, so status changes are picked up.
last_changedshows when a record last changed. - Job value and contractor are read from each permit's own page, usually within a day of filing. Early in a permit's life either can be
null. - Privacy. Homeowner and applicant names are never collected. Contractors are businesses with public state licenses.
- Accuracy. Records are public data as published by each building department.
source_urlpoints to where each one came from.
Errors
Payment is only settled when a call succeeds. If a paid call returns an error, your signed payment is not collected.
| Code | Meaning |
|---|---|
402 | Payment required. Read the PAYMENT-REQUIRED header, pay, and retry. |
400 | A parameter isn't valid, such as an unknown trade. The message lists the allowed values. |
404 | No permit or contractor with that key. |
422 | A parameter has the wrong format, such as limit above 25. |