PermisAPI
PermisAPI — free-tier JSON endpoint
PermisAPI offers a free API tier. We verified JSON on a sample GET (Government). Check provider docs for key requirements and limits.
We probe a documented GET and expect 2xx JSON — full uptime and health score. Open provider docs ↗
On our probe schedule. Uptime charts appear after the first check lands.
GET https://api.permisapi.fr/.well-known/openapi
{
"openapi": "3.1.0",
"info": {
"title": "PermisAPI",
"description": "API de permis de construire de France. Données Sitadel enrichies (geocoding BAN, SIRENE). Source : SDES, base Sitadel, data.gouv.fr.",
"version": "0.1.0"
},
"paths": {
"/": {
"get": {
"tags": [
"health"
],
"summary": "Redirect to docs",
"operationId": "root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/.well-known/openapi": {
"get": {
"tags": [
"discovery"
],
"summary": "Schéma OpenAPI 3 (autodecouvrabilité standard RFC 8615)",
"description": "Retourne le schéma OpenAPI complet de l'API (équivalent à\n/openapi.json mais à un URL bien-connu standard).\n\nPermet aux outils Postman, Insomnia, Swagger UI, et aux crawlers\nd'API de découvrir automatiquement la surface de PermisAPI sans\nconfiguration manuelle. Cohérent avec l'autre URL bien-connu déjà\nexposé : /.well-known/oauth-authorization-server (côté MCP SSE).",
"operationId": "well_known_openapi__well_known_openapi_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/health": {
"get": {
"tags": [
"health"
],
"summary": "Health check (public)",
"operationId": "health_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/v1/permits": {
"get": {
"tags": [
"permits"
],
"summary": "Liste paginee de permis avec filtres",
"description": "Recherche paginee de permis.\n\nLes filtres se combinent en AND.\n\nTri : prefixer par `-` pour descendant, sinon ascendant.\nDefault : `-date_reelle_autorisation` (les plus recents en tete).\n\nChamps de tri autorises (cf SORT_WHITELIST) :\n - `date_reelle_autorisation`\n - `date_depot`\n - `an_depot`\n - `superficie_terrain`\n\nTout autre nom retourne 422 (et NON ignore silencieusement, pour\neviter le piege \"je crois trier mais en fait non\").\n\nFiltre `min_score` (Pro+) : permet de shortlister directement les\npermis avec un Score Opportunite MDB >= N. Cout quota special :\n`limit` unites au lieu de 1, pour refleter le travail equivalent\na N appels /score individuels qui auraient ete necessaires\ncote client. Header `X-RateLimit-Cost` confirme le cout decompte.\n\nFiltre `max_risk` (Pro+) : exclut les permis dont la commune est\nclassee au-dessus du tier specifie (low / moderate / high /\ncritical). Exemple `?max_risk=moderate` ne retourne que les permis\nen zones low ou moderate. Cout quota = limit (anti-abus, equivalent\na N appels /risks). Lookup via colonne materialisee\n`permits.risk_tier` (cf migration 0015 + scripts/materialize_risks.py).\nPermis dont la commune n'est pas encore en cache sont exclus\n(ils seront inclus apres le prochain materialize_risks).\n\nFiltre `plu_zone_type` (Pro+) : prefix de zonage urbanisme PLU\nparmi U / AU / A / N / OTHER. Exemple `?plu_zone_type=U` retourne\nuniquement les permis en zone urbaine constructible. Cout quota =\nlimit (equivalent a N appels /plu).\n\nFiltre `plu_constructible` (Pro+) : booleen. `?plu_constructible=true`\nretourne uniquement les permis ou le verdict heuristique est\nconstructible (zone U ou AU). Pratique combine avec `min_score`\npour shortlister les vraies opportunites MDB. Cout quota = limit.\n\nPermis dont le PLU n'est pas encore en cache sont exclus.",
"operationId": "list_permits_v1_permits_get",
"parameters": [
{
"name": "dep_code",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
"maxLength": 5
},
{
"type": "null"
}
],
"title": "Dep Code"
}
},
{
"name": "comm_code",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
"maxLength": 10
},
{
"type": "null"
}
],
"title": "Comm Code"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PageResponse_PermitSummary_"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/permits/export": {
"get": {
"tags": [
"permits"
],
"summary": "Export bulk CSV (plans Business et +)",
"description": "Export streaming d'un sous-ensemble de permis au format CSV.\n\nRéservé aux plans Business+. Colonnes fixes (cf EXPORT_CSV_COLUMNS).\nLa limite par defaut est `plan_gating.export_max_rows(plan)` : 100k\npour Business, 1M pour Enterprise. Le user peut reduire cette limite\nvia `max_rows` mais pas l'augmenter.\n\nStreaming: on généré les lignes au fur et a mesure pour eviter d'OOM\nle processus sur un export de 1M lignes.",
"operationId": "export_permits_v1_permits_export_get",
"parameters": [
{
"name": "dep_code",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
"maxLength": 5
},
{
"type": "null"
}
],
"title": "Dep Code"
}
},
{
"name": "comm_code",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
"maxLength": 10
},
{
"type": "null"
}
],
"title": "Comm Code"
}
}
],
"responses": {
"200": {
"description": "Successful Response"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/permits/near": {
"get": {
"tags": [
"permits"
],
"summary": "Permis géocodés dans un rayon (PostGIS ST_DWithin)",
"description": "Retourne les permis dans un rayon autour d'un point lat/lng.\n\nUtilise `ST_DWithin(geom::geography, ...)` pour une distance en metres\nprecise (et non en degres). Trie par distance croissante.\n\nPlan gating : si le plan de l'user restreint les départements, on\nfiltre la query par `dep_code IN allowed_deps`. Un Free qui fait un\n`near` centre sur Lyon ne recevra donc rien (puisque Free = Paris\nseul).",
"operationId": "permits_near_v1_permits_near_get",
"parameters": [
{
"name": "lat",
"in": "query",
"required": true,
"schema": {
"type": "number",
"maximum": 90,
"minimum": -90,
"title": "Lat"
}
},
{
"name": "lng",
"in": "query",
"required": true,
"schema": {
"type": "number",
"maximum": 180,
"minimum": -180,
"title": "Lng"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermitNear"
},
"title": "Response Permits Near V1 Permits Near Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/permits/by-cadastre": {
"get": {
"tags": [
"permits"
],
"summary": "Permis pour une parcelle cadastrale donnée",
"description": "Permis qui referencent la parcelle cadastrale (section + numéro).\n\nFeature réservée aux plans Pro et superieurs (cf pricing landing).",
"operationId": "permits_by_cadastre_v1_permits_by_cadastre_get",
"parameters": [
{
"name": "section",
"in": "query",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 10,
"title": "Section"
}
},
{
"name": "numero",
"in": "query",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"title": "Numero"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermitSummary"
},
"title": "Response Permits By Cadastre V1 Permits By Cadastre Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/permits/{num_pa}": {
"get": {
"tags": [
"permits"
],
"summary": "Détail d'un permis par son identifiant Sitadel",
"operationId": "get_permit_v1_permits__num_pa__get",
"parameters": [
{
"name": "num_pa",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 50,
"title": "Num Pa"
}
},
{
"name": "X-API-Key",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Api-Key"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PermitResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AlertCreate": {
"properties": {
"name": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"title": "Name"
},
"center_lat": {
"type": "number",
"maximum": 90,
"minimum": -90,
"title": "Center Lat"
},
"center_lng": {
"type": "number",
"maximum": 180,
"minimum": -180,
"title": "Center Lng"
},
"radius_km": {
"type": "number",
"maximum": 200,
"exclusiveMinimum": 0,
"title": "Radius Km",
"default": 5
},
"webhook_url": {
"type": "string",
"maxLength": 500,
"minLength": 8,
"title": "Webhook Url"
},
"permit_types": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Permit Types",
"description": "Liste de permit_type a filtrer (ex: ['PC_LOGEMENT']). None = tous."
},
"min_superficie": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
],
"title": "Min Superficie"
},
"etat_pa": {
"anyOf": [
{
"type": "integer",
"maximum": 6,
"minimum": 1
},
{
"type": "null"
}
],
"title": "Etat Pa"
}
},
"type": "object",
"required": [
"name",
"center_lat"
],
"title": "AlertCreate",
"description": "Payload pour créer une alerte webhook."
},
"AlertResponse": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"name": {
"type": "string",
"title": "Name"
},
"center_lat": {
"type": "number",
"title": "Center Lat"
},
"center_lng": {
"type": "number",
"title": "Center Lng"
},
"radius_km": {
"type": "number",
"title": "Radius Km"
},
"webhook_url": {
"type": "string",
"title": "Webhook Url"
},
"permit_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Permit Types"
},
"min_superficie": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Min Superficie"
}
},
"type": "object",
"required": [
"id",
"name"
],
"title": "AlertResponse",
"description": "Une alerte renvoyee par l'API (sans le secret)."
},
"AlertUpdate": {
"properties": {
"name": {
"anyOf": [
{
"type": "string",
"maxLength": 100,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Name"
},
"is_active": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Active"
},
"radius_km": {
"anyOf": [
{
"type": "number",
"maximum": 200,
"exclusiveMinimum": 0
},
{
"type": "null"
}
],
"title": "Radius Km"
},
"webhook_url": {
"anyOf": [
{
"type": "string",
"maxLength": 500,
"minLength": 8
},
{
"type": "null"
}
],
"title": "Webhook Url"
},
"permit_types": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Permit Types"
},
"min_superficie": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
],
"title": "Min Superficie"
},
"etat_pa": {
"anyOf": [
{
"type": "integer",
"maximum": 6,
"minimum": 1
},
{
"type": "null"
}
],
"title": "Etat Pa"
}
},
"type": "object",
"title": "AlertUpdate",
"description": "Update partielle d'une alerte existante."
},
"AlertWithSecret": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"name": {
"type": "string",
"title": "Name"
},
"center_lat": {
"type": "number",
"title": "Center Lat"
},
"center_lng": {
"type": "number",
"title": "Center Lng"
},
"radius_km": {
"type": "number",
"title": "Radius Km"
},
"webhook_url": {
"type": "string",
"title": "Webhook Url"
},
"permit_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Permit Types"
},
"min_superficie": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Min Superficie"
}
},
"type": "object",
"required": [
"id",
"name"
],
"title": "AlertWithSecret",
"description": "Retournee UNE SEULE FOIS a la creation (le secret en clair)."
},
"BrandingByDomainResponse": {
"properties": {
"domain": {
"type": "string",
"title": "Domain",
"description": "Le domain interroge (echo input)"
},
"branded": {
"type": "boolean",
"title": "Branded",
"description": "True si un client Enterprise est configure pour ce domain"
},
"logo_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Logo Url"
},
"primary_color": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Primary Color"
},
"footer_text": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Footer Text"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"secondary_color": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secondary Color",
"description": "Override var(--accent-secondary)"
},
"background_color": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Background Color",
"description": "Override var(--bg-branded)"
}
},
"type": "object",
"required": [
"domain",
"branded"
],
"title": "BrandingByDomainResponse",
"description": "Branding visuel pour un domain donne (V0.4 dashboard brande + V0.6b\npalette 4 couleurs).\n\nTous les champs `null` = pas de branding configure pour ce domain\n(le frontend doit afficher le branding PermisAPI par defaut)."
},
"BrandingConfigPutBody": {
"properties": {
"logo_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Logo Url",
"description": "URL HTTPS du logo client (PNG ou SVG, ~300x60 px recommande). Affiche dans le header du dashboard brande a la place de 'PermisAPI'."
},
"primary_color": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Primary Color",
"description": "Couleur primaire au format hex `#RRGGBB` (ex `#FF6B35`). Remplace l'ambre PermisAPI dans le dashboard brande (CTAs, focus)."
},
"footer_text": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Footer Text",
"description": "Texte du footer (max 500 chars). Remplace le footer PermisAPI par ce texte dans le dashboard brande."
},
"color_secondary": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Color Secondary",
"description": "Couleur secondaire hex `#RRGGBB` pour CTAs secondaires et accents discrets. Override var(--accent-secondary)."
},
"color_background": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Color Background",
"description": "Couleur de fond hex `#RRGGBB` pour le dashboard brande. Override var(--bg-branded). Permet de matcher la charte d'une fonciere ou banque (fond clair / neutre vs sombre PermisAPI)."
},
"color_hover": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Color Hover",
"description": "Couleur hover hex `#RRGGBB` pour les states interactifs. Override var(--accent-hover). Typiquement version plus saturee de la couleur primaire."
}
},
"type": "object",
"title": "BrandingConfigPutBody",
"description": "Payload pour configurer le branding dashboard (V0.4 + V0.6b palette).\n\nTous les champs sont optionnels et nullable :\n - `null` (explicite) : reset le champ a sa valeur par defaut PermisAPI\n - Champ absent du payload : ne modifie pas le champ existant"
},
"BulkEnrichInputRow": {
"properties": {
"ref": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"title": "Ref",
"description": "Reference client (echo dans la response, pour mapper)"
},
"lat": {
"anyOf": [
{
"type": "number",
"maximum": 90,
"minimum": -90
},
{
"type": "null"
}
],
"title": "Lat"
},
"lng": {
"anyOf": [
{
"type": "number",
"maximum": 180,
"minimum": -180
},
{
"type": "null"
}
],
"title": "Lng"
},
"adresse": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Adresse"
},
"commune": {
"anyOf": [
{
"type": "string",
"maxLength": 10
},
{
"type": "null"
}
],
"title": "Commune"
},
"section": {
"anyOf": [
{
"type": "string",
"maxLength": 10
},
{
"type": "null"
}
],
"title": "Section"
},
"numero": {
"anyOf": [
{
"type": "string",
"maxLength": 20
},
{
"type": "null"
}
],
"title": "Numero"
}
},
"type": "object",
"required": [
"ref"
],
"title": "BulkEnrichInputRow",
"description": "Une ligne d'input. 1 des 3 modes doit etre rempli :\n- mode coordonnees : lat + lng\n- mode adresse : adresse\n- mode cadastre : commune + section + numero"
},
"BulkEnrichMatch": {
"properties": {
"num_pa": {
"type": "string",
"title": "Num Pa"
},
"distance_m": {
"type": "number",
"title": "Distance M"
},
"permit_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Permit Type"
},
"date_reelle_autorisation": {
"anyOf": [
{
"type": "string",
"format": "date"
},
{
"type": "null"
}
],
"title": "Date Reelle Autorisation"
},
"score_mdb": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Score Mdb"
},
"score_mdb_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Score Mdb Tier"
},
"risk_score": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Risk Score"
},
"risk_tier": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Risk Tier"
}
},
"type": "object",
"required": [
"num_pa",
"distance_m"
],
"title": "BulkEnrichMatch",
"description": "Un permis qui matche une ligne d'input."
}
}
},
"tags": [
{
"name": "permits",
"description": "Recherche et détail de permis."
},
{
"name": "stats",
"description": "Stats agrégées par territoire."
}
]
}curl "https://api.permisapi.fr/.well-known/openapi"const res = await fetch("https://api.permisapi.fr/.well-known/openapi");
const data = await res.json();
console.log(data);import requests
res = requests.get("https://api.permisapi.fr/.well-known/openapi")
print(res.json())BROWSER CALLS BLOCKED (NO CORS/HTTPS) — USE THE CURL SNIPPET
/.well-known/openapiPROBEDPermisAPI — free-tier JSON endpoint
/.well-knownWell Known — documented GET route.
/.well-known/1.Well Known details by ID (example: 1).
/.well-known?limit=10Well Known — documented GET route.
/.well-known/search?q=testSearch by query parameters.
/Redirect to docs
We probe a documented GET and expect 2xx JSON — full uptime and health score. Export includes every documented route below.
Not tracked yet. Shape-change history starts once this API joins our probe schedule.
PermisAPI: common questions
Is PermisAPI free to use?
Yes — PermisAPI is a free government API. Free tier: Free tier — API key may be required for production. Whether the free tier allows commercial use is unclear — check the provider docs.
Does PermisAPI need an API key?
Yes — PermisAPI needs a free API key, which you pass on each request. Rate limits: Unpublished.
Can I call PermisAPI from the browser?
Not directly — PermisAPI doesn't send browser-friendly CORS headers, so call it from a server or proxy instead. Copy the curl or Python snippet on this page to get started.
Is PermisAPI up right now?
PermisAPI is catalogued but not yet on our probe schedule, so we don't publish a live status for it. Check the provider's own status page or docs for its current state.