<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation — sfpermits.ai</title>
<meta name="description" content="sfpermits.ai API documentation. 34 tools for San Francisco building permit intelligence. Connect via Claude.ai in 3 steps.">
{% include "fragments/head_obsidian.html" %}
<style nonce="{{ csp_nonce }}">
body {
font-family: var(--sans);
background: var(--obsidian);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
margin: 0;
}
/* ── Hero ── */
.docs-hero {
padding: var(--space-16) 0 var(--space-10);
border-bottom: 1px solid var(--glass-border);
}
.docs-hero .eyebrow {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.1em;
margin: 0 0 var(--space-3) 0;
}
.docs-hero h1 {
font-family: var(--sans);
font-size: var(--text-2xl);
font-weight: 300;
line-height: 1.2;
color: var(--text-primary);
margin: 0 0 var(--space-4) 0;
}
.docs-hero p {
font-family: var(--sans);
font-size: var(--text-lg);
color: var(--text-secondary);
max-width: 640px;
line-height: 1.7;
margin: 0 0 var(--space-6) 0;
}
.hero-stats {
display: flex;
gap: var(--space-8);
flex-wrap: wrap;
}
.hero-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.hero-stat .val {
font-family: var(--mono);
font-size: var(--text-xl);
font-weight: 300;
color: var(--accent);
}
.hero-stat .lbl {
font-family: var(--sans);
font-size: var(--text-xs);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ── Quick Start ── */
.quickstart {
padding: var(--space-12) 0 var(--space-8);
border-bottom: 1px solid var(--glass-border);
}
.section-heading {
font-family: var(--mono);
font-size: var(--text-base);
font-weight: 400;
color: var(--accent);
margin: 0 0 var(--space-6) 0;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-4);
}
.step-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: var(--space-5);
}
.step-num {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--accent);
font-weight: 400;
margin: 0 0 var(--space-2) 0;
letter-spacing: 0.08em;
}
.step-card h3 {
font-family: var(--sans);
font-size: var(--text-base);
font-weight: 500;
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
}
.step-card p {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0;
line-height: 1.6;
}
.step-card code {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--accent);
background: var(--obsidian-light);
border: 1px solid var(--glass-border);
border-radius: 3px;
padding: 2px 6px;
display: inline-block;
margin-top: var(--space-2);
word-break: break-all;
}
/* ── Tool catalog ── */
.catalog {
padding: var(--space-12) 0;
}
.category-nav {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-8);
padding-bottom: var(--space-6);
border-bottom: 1px solid var(--glass-border);
}
.category-nav a {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-secondary);
text-decoration: none;
padding: var(--space-1) var(--space-3);
border: 1px solid var(--glass-border);
border-radius: 20px;
transition: color 0.15s, border-color 0.15s;
}
.category-nav a:hover {
color: var(--accent);
border-color: var(--accent-ring);
}
.category-section {
margin-bottom: var(--space-16);
}
.category-header {
display: flex;
align-items: baseline;
gap: var(--space-3);
margin-bottom: var(--space-2);
}
.category-header h2 {
font-family: var(--mono);
font-size: var(--text-base);
font-weight: 400;
color: var(--accent);
margin: 0;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.category-count {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.category-desc {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0 0 var(--space-6) 0;
line-height: 1.6;
}
.tool-grid {
display: grid;
gap: var(--space-4);
}
.tool-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: 8px;
overflow: hidden;
}
.tool-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--glass-border);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
}
.tool-name {
font-family: var(--mono);
font-size: var(--text-base);
font-weight: 400;
color: var(--text-primary);
}
.tool-body {
padding: var(--space-4) var(--space-5);
}
.tool-desc {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0 0 var(--space-4) 0;
line-height: 1.6;
}
.params-heading {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 0 0 var(--space-2) 0;
}
.params-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-xs);
margin-bottom: var(--space-4);
}
.params-table th {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.06em;
text-align: left;
padding: var(--space-1) var(--space-2);
border-bottom: 1px solid var(--glass-border);
}
.params-table td {
font-family: var(--sans);
font-size: var(--text-xs);
color: var(--text-secondary);
padding: var(--space-1) var(--space-2);
border-bottom: 1px solid var(--glass-border);
vertical-align: top;
}
.params-table td:first-child {
font-family: var(--mono);
color: var(--text-primary);
white-space: nowrap;
}
.params-table td:nth-child(2) {
color: var(--accent);
white-space: nowrap;
}
.badge-required {
font-family: var(--mono);
font-size: 9px;
font-weight: 400;
color: var(--signal-amber);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: 3px;
padding: 1px 5px;
white-space: nowrap;
}
.badge-optional {
font-family: var(--mono);
font-size: 9px;
color: var(--text-tertiary);
border: 1px solid var(--glass-border);
border-radius: 3px;
padding: 1px 5px;
white-space: nowrap;
}
.example-query {
background: var(--obsidian-light);
border: 1px solid var(--glass-border);
border-radius: 4px;
padding: var(--space-3) var(--space-4);
margin-top: var(--space-2);
}
.example-label {
font-family: var(--mono);
font-size: 10px;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: var(--space-1);
}
.example-text {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
font-style: italic;
}
/* ── Rate limits + Auth ── */
.info-sections {
padding: var(--space-12) 0;
border-top: 1px solid var(--glass-border);
}
.info-section {
margin-bottom: var(--space-12);
}
.info-section h2 {
font-family: var(--mono);
font-size: var(--text-base);
font-weight: 400;
color: var(--accent);
margin: 0 0 var(--space-4) 0;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.tier-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
max-width: 560px;
}
.tier-table th {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.08em;
text-align: left;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--glass-border);
}
.tier-table td {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--glass-border);
}
.tier-table td:first-child {
font-family: var(--mono);
color: var(--text-primary);
}
.auth-steps {
counter-reset: auth-step;
list-style: none;
padding: 0;
margin: 0;
max-width: 560px;
}
.auth-steps li {
counter-increment: auth-step;
display: flex;
gap: var(--space-4);
align-items: flex-start;
margin-bottom: var(--space-4);
}
.auth-steps li::before {
content: counter(auth-step);
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--accent);
background: var(--obsidian-mid);
border: 1px solid var(--accent-ring);
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.auth-steps li span {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.6;
}
.info-link {
color: var(--accent);
text-decoration: none;
}
.info-link:hover {
text-decoration: underline;
}
/* ── Layout ── */
.container {
max-width: 960px;
margin: 0 auto;
padding: 0 var(--space-5);
}
/* ── No params ── */
.no-params {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
font-style: italic;
margin: 0 0 var(--space-4) 0;
}
@media (max-width: 640px) {
.docs-hero h1 { font-size: var(--text-xl); }
.hero-stats { gap: var(--space-5); }
.steps { grid-template-columns: 1fr; }
.params-table { display: block; overflow-x: auto; }
}
</style>
</head>
<body>
{% include "fragments/nav.html" %}
<div class="container">
<!-- Hero -->
<section class="docs-hero">
<p class="eyebrow">API Documentation</p>
<h1>sfpermits.ai — San Francisco Permit Intelligence</h1>
<p>
{{ catalog.total_tools }} tools for querying SF building permits, predicting timelines, estimating fees,
analyzing plan sets, and navigating the city's permitting system. Built on 18M+ public records
updated nightly from 22 government data sources.
</p>
<div class="hero-stats">
<div class="hero-stat">
<span class="val">{{ catalog.total_tools }}</span>
<span class="lbl">Tools</span>
</div>
<div class="hero-stat">
<span class="val">{{ catalog.total_categories }}</span>
<span class="lbl">Categories</span>
</div>
<div class="hero-stat">
<span class="val">18M+</span>
<span class="lbl">Records</span>
</div>
<div class="hero-stat">
<span class="val">Nightly</span>
<span class="lbl">Data refresh</span>
</div>
</div>
</section>
<!-- Quick Start -->
<section class="quickstart">
<p class="section-heading">Connect via Claude.ai</p>
<div class="steps">
<div class="step-card">
<p class="step-num">Step 01</p>
<h3>Open Claude.ai Settings</h3>
<p>Go to <strong>Settings → Integrations</strong> and click <strong>Add custom connector</strong>.</p>
</div>
<div class="step-card">
<p class="step-num">Step 02</p>
<h3>Enter the MCP URL</h3>
<p>Paste the server URL:</p>
<code>https://sfpermits-mcp-api-production.up.railway.app/mcp</code>
</div>
<div class="step-card">
<p class="step-num">Step 03</p>
<h3>Authorize via OAuth</h3>
<p>Claude will prompt you to authorize. Sign in or create an sfpermits.ai account to complete the OAuth flow.</p>
</div>
</div>
</section>
<!-- Tool Catalog -->
<section class="catalog">
<p class="section-heading">Tool Catalog</p>
<!-- Category nav -->
<nav class="category-nav" aria-label="Tool categories">
{% for cat in catalog.categories %}
<a href="#{{ cat.id }}">{{ cat.name }} ({{ cat.tools | length }})</a>
{% endfor %}
</nav>
<!-- Categories -->
{% for cat in catalog.categories %}
<div class="category-section" id="{{ cat.id }}">
<div class="category-header">
<h2>{{ cat.name }}</h2>
<span class="category-count">{{ cat.tools | length }} tools</span>
</div>
<p class="category-desc">{{ cat.description }}</p>
<div class="tool-grid">
{% for tool in cat.tools %}
<div class="tool-card">
<div class="tool-header">
<span class="tool-name">{{ tool.name }}</span>
</div>
<div class="tool-body">
<p class="tool-desc">{{ tool.description }}</p>
{% if tool.parameters %}
<p class="params-heading">Parameters</p>
<table class="params-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for param in tool.parameters %}
<tr>
<td>{{ param.name }}</td>
<td>{{ param.type }}</td>
<td>
{% if param.required %}
<span class="badge-required">required</span>
{% else %}
<span class="badge-optional">optional</span>
{% endif %}
</td>
<td>{{ param.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="no-params">No parameters required</p>
{% endif %}
{% if tool.example %}
<div class="example-query">
<p class="example-label">Example query</p>
<p class="example-text">"{{ tool.example }}"</p>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</section>
<!-- Rate Limits + Auth -->
<section class="info-sections">
<div class="info-section">
<h2>Rate Limits</h2>
<table class="tier-table">
<thead>
<tr>
<th>Tier</th>
<th>Limit</th>
<th>Access</th>
</tr>
</thead>
<tbody>
<tr>
<td>Demo</td>
<td>10 calls / day</td>
<td>Unauthenticated or trial accounts</td>
</tr>
<tr>
<td>Professional</td>
<td>1,000 calls / day</td>
<td>Beta accounts via OAuth</td>
</tr>
<tr>
<td>Unlimited</td>
<td>Unrestricted</td>
<td>Internal / admin accounts</td>
</tr>
</tbody>
</table>
<p style="font-family: var(--sans); font-size: var(--text-sm); color: var(--text-secondary); margin-top: var(--space-3);">
Exceeding limits returns HTTP 429. Limits reset at midnight Pacific time.
Contact <a href="mailto:tim@sfpermits.ai" class="info-link">tim@sfpermits.ai</a> to request a higher tier.
</p>
</div>
<div class="info-section">
<h2>Authentication</h2>
<p style="font-family: var(--sans); font-size: var(--text-sm); color: var(--text-secondary); margin: 0 0 var(--space-4) 0;">
The MCP server uses OAuth 2.1 with PKCE and dynamic client registration per the
MCP authorization specification. Claude.ai handles this automatically.
</p>
<ol class="auth-steps">
<li><span>Claude.ai discovers the authorization server at <code style="font-family:var(--mono);color:var(--accent);font-size:var(--text-xs);">/.well-known/oauth-authorization-server</code></span></li>
<li><span>Claude registers as a client via <code style="font-family:var(--mono);color:var(--accent);font-size:var(--text-xs);">POST /register</code> (dynamic client registration)</span></li>
<li><span>You authorize via the sfpermits.ai login page (magic-link email)</span></li>
<li><span>Claude receives an access token and begins making authenticated tool calls</span></li>
</ol>
<p style="font-family: var(--sans); font-size: var(--text-sm); color: var(--text-secondary); margin: var(--space-4) 0 0 0;">
See <a href="/privacy" class="info-link">Privacy Policy</a> and
<a href="/terms" class="info-link">Terms of Service</a> for data handling details.
</p>
</div>
</section>
</div>
<footer style="border-top: 1px solid var(--glass-border); padding: var(--space-8) 0; margin-top: var(--space-8);">
<div style="max-width: 960px; margin: 0 auto; padding: 0 var(--space-5); display: flex; gap: var(--space-6); flex-wrap: wrap;">
<a href="/privacy" style="font-family: var(--mono); font-size: var(--text-xs); color: var(--text-secondary); text-decoration: none;">Privacy</a>
<a href="/terms" style="font-family: var(--mono); font-size: var(--text-xs); color: var(--text-secondary); text-decoration: none;">Terms</a>
<span style="font-family: var(--mono); font-size: var(--text-xs); color: var(--text-ghost);">sfpermits.ai — SF permit data from data.sfgov.org</span>
</div>
</footer>
</body>
</html>