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.

New permits, newest first. Within each day, permits with a job value and contractor come first. Up to 25 per call; page with offset.

ParameterWhat it does
tradeOne of roofing, hvac, solar, pool, electrical, plumbing, windows_doors, new_construction, remodel, demolition
since, untilFiled on or after / on or before a date, YYYY-MM-DD
min_valueMinimum job value in US dollars
zip, cityLimit to a zip code or city
jurisdictionA source key from /coverage, such as tampa or pinellas
qWords in the work description, such as heat pump
include_applicationstrue to also return applications not yet issued a permit number (off by default)
limit, offsetPage 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.01

One 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.05

Every 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.10

Every 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.

ToolWhat it returnsPer call
coverageCities covered, permit counts and last update timeFree
sample_permitsThree recent permits, to see the record formatFree
search_permitsThe same search as GET /permits/search$0.02
get_permitOne permit with its recorded history$0.01
property_historyEvery permit at one address$0.05
contractor_historyA 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 /coverageFree

The cities covered today, permit counts, date range, and when each was last updated. Check it before relying on a city.

GET /sampleFree

Three recent permits, so you can see the record format before paying.

GET /healthFree

status says whether the API is up; collection says whether the data is fresh, with any problems listed.

The permit record

Errors

Payment is only settled when a call succeeds. If a paid call returns an error, your signed payment is not collected.

CodeMeaning
402Payment required. Read the PAYMENT-REQUIRED header, pay, and retry.
400A parameter isn't valid, such as an unknown trade. The message lists the allowed values.
404No permit or contractor with that key.
422A parameter has the wrong format, such as limit above 25.