Démarrage rapide
Toutes les requêtes se font en HTTPS vers {{BASE}}/v1, avec un corps JSON et votre clé dans l'en-tête X-API-Key. Exemple : cinq boucles de trail de 10 km au départ d'Angers.
curl -X POST {{BASE}}/v1/routes \
-H "X-API-Key: dplus_votre_cle" \
-H "Content-Type: application/json" \
-d '{
"lat": 47.4739,
"lon": -0.5516,
"radius_km": 5,
"terrain": "trail",
"mode": "distance",
"target_distance_km": 10
}'
// Côté serveur (Node 18+) : ne jamais exposer la clé dans un navigateur const res = await fetch("{{BASE}}/v1/routes", { method: "POST", headers: { "X-API-Key": process.env.DPLUS_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ lat: 47.4739, lon: -0.5516, radius_km: 5, terrain: "trail", mode: "distance", target_distance_km: 10, }), }); if (!res.ok) throw new Error((await res.json()).detail); const { routes } = await res.json(); routes.forEach((r) => console.log(r.label, r.stats.distance_km, "km", r.stats.elevation_gain_m, "m D+"));
import os, httpx res = httpx.post( "{{BASE}}/v1/routes", headers={"X-API-Key": os.environ["DPLUS_API_KEY"]}, json={ "lat": 47.4739, "lon": -0.5516, "radius_km": 5, "terrain": "trail", "mode": "distance", "target_distance_km": 10, }, timeout=120, ) res.raise_for_status() for r in res.json()["routes"]: print(r["label"], r["stats"]["distance_km"], "km", r["stats"]["elevation_gain_m"], "m D+")
Quick start
All requests go over HTTPS to {{BASE}}/v1, with a JSON body and your key in the X-API-Key header. Example: five 10 km trail loops starting from Angers.
curl -X POST {{BASE}}/v1/routes \
-H "X-API-Key: dplus_votre_cle" \
-H "Content-Type: application/json" \
-d '{
"lat": 47.4739,
"lon": -0.5516,
"radius_km": 5,
"terrain": "trail",
"mode": "distance",
"target_distance_km": 10
}'
// Server side (Node 18+): never expose the key in a browser const res = await fetch("{{BASE}}/v1/routes", { method: "POST", headers: { "X-API-Key": process.env.DPLUS_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ lat: 47.4739, lon: -0.5516, radius_km: 5, terrain: "trail", mode: "distance", target_distance_km: 10, }), }); if (!res.ok) throw new Error((await res.json()).detail); const { routes } = await res.json(); routes.forEach((r) => console.log(r.label, r.stats.distance_km, "km", r.stats.elevation_gain_m, "m D+"));
import os, httpx res = httpx.post( "{{BASE}}/v1/routes", headers={"X-API-Key": os.environ["DPLUS_API_KEY"]}, json={ "lat": 47.4739, "lon": -0.5516, "radius_km": 5, "terrain": "trail", "mode": "distance", "target_distance_km": 10, }, timeout=120, ) res.raise_for_status() for r in res.json()["routes"]: print(r["label"], r["stats"]["distance_km"], "km", r["stats"]["elevation_gain_m"], "m D+")
Authentification
Chaque requête porte votre clé dans l'en-tête X-API-Key. Les clés commencent par dplus_ et vous sont transmises une seule fois, à la création. Demandez la vôtre en décrivant votre projet.
Authentication
Every request carries your key in the X-API-Key header. Keys start with dplus_ and are sent to you once, when created. Request yours by describing your project.
Quotas et débit
- Deux quotas : les générations réussies de
POST /v1/routes, et les conversations avec l'assistant (POST /v1/assistant), comptés séparément. Les compteurs repartent à zéro le 1er de chaque mois (UTC) ; pour un accès de démonstration, ils valent pour toute sa durée. Les fichiers GPX et la consultation de la consommation sont hors quota. - Débit : un nombre limité d'appels par minute et par clé. Au-delà, l'API répond
429avec l'en-têteRetry-After.
Chaque réponse réussie indique où vous en êtes :
X-Quota-Limit: 1000 X-Quota-Remaining: 987
Quotas and rate limits
- Two quotas: successful
POST /v1/routesgenerations, and assistant conversations (POST /v1/assistant), counted separately. Counters reset on the 1st of each month (UTC); for a demo access, they cover its whole duration. GPX files and usage queries do not count. - Rate limit: a limited number of calls per minute and per key. Beyond that, the API returns
429with aRetry-Afterheader.
Every successful response tells you where you stand:
X-Quota-Limit: 1000 X-Quota-Remaining: 987
/v1/routes
Calcule jusqu'à cinq parcours nettement différents, classés du meilleur au moins bon. Seuls lat, lon et radius_km sont obligatoires ; l'objectif dépend de mode. Le calcul prend en général de 1 à 20 secondes, jusqu'à 40 secondes pour plus de 100 km : prévoyez un délai d'attente d'au moins 120 secondes.
Paramètres principaux
| Champ | Type | Description |
|---|---|---|
lat, lonrequis | nombre | Point de départ, ou centre de la zone de recherche si within_radius est vrai. |
radius_kmrequis | 1 – 30 | Rayon de la zone dans laquelle le parcours reste. |
terrain | texte | road (bitume, par défaut), trail (chemins et sentiers) ou mixed (indifférent). C'est une préférence : le parcours en suit le plus possible. |
mode | texte | Objectif du calcul, voir ci-dessous. Par défaut distance. |
target_distance_km | 1 – 200 | Distance visée. Les parcours font entre cette distance et 500 m de plus. |
target_elevation_m | 50 – 10 000 | Dénivelé positif visé, en mètres. |
within_radius | booléen | false (défaut) : départ exactement à lat/lon. true : D+ choisit les meilleurs départs dans le rayon (lacs, parcs, reliefs). |
preferences | objet | avoid_roads (limiter le bitume), near_water (bords de l'eau), marked_trails (sentiers balisés) : booléens. |
Modes
| mode | Champs à fournir | Résultat |
|---|---|---|
distance | target_distance_km | Parcours à la distance visée. |
elevation | target_elevation_m | Le parcours le plus court atteignant le dénivelé visé. |
combo | les deux | La distance visée avec un dénivelé aussi proche que possible de la cible. |
elevation_max | target_distance_km | Le plus de dénivelé possible sur la distance. |
Si l'objectif est hors de portée dans la zone (par exemple 1 000 m de D+ en terrain plat), les parcours les plus proches sont tout de même renvoyés et le champ message commence par « ⚠ » avec l'explication.
Parcours avancés
| Champ | Type | Description |
|---|---|---|
shape | texte | loop (boucle, défaut) ou one_way (aller simple). |
end_lat, end_lon | nombre | Aller simple : point d'arrivée. |
direction_deg | 0 – 359 | Aller simple sans arrivée : cap à suivre (0 = nord, 90 = est). |
steps | liste (8 max.) | Étapes imposées, dans l'ordre. Chacune : action = point (passer par ce point), pass (passer à proximité), through (le traverser), inside (y rester, la distance se faisant à l'intérieur), tour (en faire le tour) ou laps (plusieurs tours, avec laps de 1 à 10), et lat/lon. Pour le tour d'un hameau ou d'un village, ajoutez radius_m (50 – 5 000). |
variant | 0 – 100 | « Autres itinéraires » : numéro de la série demandée (0 = première). |
exclude_routes | liste | Tracés déjà proposés ([[lon, lat], …]) : les nouveaux parcours s'en écartent. |
Exemple : 15 km avec le plus de D+ possible et un passage imposé
{
"lat": 47.4739, "lon": -0.5516, "radius_km": 8,
"terrain": "trail",
"mode": "elevation_max",
"target_distance_km": 15,
"steps": [
{ "action": "point", "lat": 47.4806, "lon": -0.5925, "name": "Étang Saint-Nicolas" }
],
"preferences": { "near_water": true }
}/v1/routes
Computes up to five clearly different routes, ranked from best to worst. Only lat, lon and radius_km are required; the goal depends on mode. Computing usually takes 1 to 20 seconds, up to 40 seconds beyond 100 km: set a timeout of at least 120 seconds.
Main parameters
| Field | Type | Description |
|---|---|---|
lat, lonrequired | number | Starting point, or centre of the search area when within_radius is true. |
radius_kmrequired | 1 – 30 | Radius of the area the route stays within. |
terrain | string | road (tarmac, default), trail (tracks and trails) or mixed (either). It is a preference: the route follows it as much as possible. |
mode | string | Goal of the computation, see below. Defaults to distance. |
target_distance_km | 1 – 200 | Target distance. Routes are between this distance and 500 m more. |
target_elevation_m | 50 – 10 000 | Target elevation gain, in metres. |
within_radius | boolean | false (default): start exactly at lat/lon. true: D+ picks the best starts within the radius (lakes, parks, hills). |
preferences | object | avoid_roads (less tarmac), near_water (along water), marked_trails (waymarked trails): booleans. |
Modes
| mode | Fields to provide | Result |
|---|---|---|
distance | target_distance_km | Routes at the target distance. |
elevation | target_elevation_m | The shortest route reaching the target elevation gain. |
combo | both | The target distance with an elevation gain as close as possible to the target. |
elevation_max | target_distance_km | As much elevation gain as possible over the distance. |
If the goal is out of reach in the area (for example 1,000 m of elevation gain on flat ground), the closest routes are still returned and the message field starts with “⚠” and the explanation.
Advanced routes
| Field | Type | Description |
|---|---|---|
shape | string | loop (default) or one_way. |
end_lat, end_lon | number | One way: finish point. |
direction_deg | 0 – 359 | One way without a finish: heading to follow (0 = north, 90 = east). |
steps | list (8 max.) | Required stops, in order. Each one: action = point (go through this point), pass (go nearby), through (go across it), inside (stay in it, covering the distance inside), tour (go around it) or laps (several laps, with laps from 1 to 10), and lat/lon. To go around a hamlet or village, add radius_m (50 – 5,000). |
variant | 0 – 100 | “More routes”: number of the requested set (0 = first). |
exclude_routes | list | Routes already shown ([[lon, lat], …]): new routes keep away from them. |
Example: 15 km with as much climbing as possible and a required stop
{
"lat": 47.4739, "lon": -0.5516, "radius_km": 8,
"terrain": "trail",
"mode": "elevation_max",
"target_distance_km": 15,
"steps": [
{ "action": "point", "lat": 47.4806, "lon": -0.5925, "name": "Étang Saint-Nicolas" }
],
"preferences": { "near_water": true }
}Format de la réponse
La réponse contient la liste routes (cinq au plus, parfois moins si la zone n'offre pas assez de parcours vraiment différents) et un message de synthèse.
{
"routes": [
{
"rank": 1,
"label": "Itinéraire 1 — Meilleur",
"color": "#dc2626",
"geojson": { "type": "FeatureCollection", "features": [
{ "type": "Feature", "geometry": { "type": "LineString",
"coordinates": [[-0.551629, 47.473894, 22.1], …] } } ] },
"stats": { "distance_km": 10.44, "elevation_gain_m": 81.9, "elevation_loss_m": 80.4 },
"elevation_profile": [{ "distance_m": 0.0, "elevation_m": 22.1 }, …],
"message": "Direction NE · 42 % chemins / sentiers · 36 % au bord de l'eau",
"route_start_lat": 47.473894, "route_start_lon": -0.551629,
"laps": [],
"directions": {
"forward": { "elevation_gain_m": 81.9, "max_grade_pct": 5.7, "longest_climb_m": 3240, "biggest_climb_m": 32.7, … },
"reverse": { "elevation_gain_m": 75.4, "max_grade_pct": 6.8, … }
},
"one_way": false
}
],
"mode": "distance",
"message": "…"
}| Champ | Description |
|---|---|
geojson | Tracé en GeoJSON. Coordonnées [longitude, latitude, altitude], dans le sens de course. |
stats | Distance (km), dénivelés positif et négatif (m), calculés sur le relief IGN. |
elevation_profile | Profil d'altitude : distance depuis le départ et altitude, en mètres. |
laps | Sites parcourus plusieurs fois : name, count, label (« 2 tours ») et position pour l'afficher sur la carte. |
directions | Montées dans le sens proposé (forward) et en sens inverse (reverse) : pente maximale sur 200 m, plus longue montée, plus grosse montée. |
message | Résumé lisible : direction, sites traversés, part de chemins, de bords de l'eau, de sentiers balisés. |
Response format
The response contains the routes list (five at most, sometimes fewer when the area does not offer enough truly different routes) and a summary message.
{
"routes": [
{
"rank": 1,
"label": "Route 1 — Best",
"color": "#dc2626",
"geojson": { "type": "FeatureCollection", "features": [
{ "type": "Feature", "geometry": { "type": "LineString",
"coordinates": [[-0.551629, 47.473894, 22.1], …] } } ] },
"stats": { "distance_km": 10.44, "elevation_gain_m": 81.9, "elevation_loss_m": 80.4 },
"elevation_profile": [{ "distance_m": 0.0, "elevation_m": 22.1 }, …],
"message": "Heading NE · 42% paths and trails · 36% along water",
"route_start_lat": 47.473894, "route_start_lon": -0.551629,
"laps": [],
"directions": {
"forward": { "elevation_gain_m": 81.9, "max_grade_pct": 5.7, "longest_climb_m": 3240, "biggest_climb_m": 32.7, … },
"reverse": { "elevation_gain_m": 75.4, "max_grade_pct": 6.8, … }
},
"one_way": false
}
],
"mode": "distance",
"message": "…"
}| Field | Description |
|---|---|
geojson | Route as GeoJSON. Coordinates [longitude, latitude, elevation], in running order. |
stats | Distance (km), elevation gain and loss (m), computed on IGN terrain data. |
elevation_profile | Elevation profile: distance from the start and elevation, in metres. |
laps | Places covered several times: name, count, label (“2 laps”) and a position to show it on the map. |
directions | Climbs in the suggested direction (forward) and the other way (reverse): steepest grade over 200 m, longest climb, biggest climb. |
message | Readable summary: heading, places crossed, share of paths, water edges and waymarked trails. |
/v1/gpx
Transforme un tracé en fichier GPX 1.1, à importer dans une montre ou une application de course. Hors quota.
curl -X POST {{BASE}}/v1/gpx \
-H "X-API-Key: dplus_votre_cle" -H "Content-Type: application/json" \
-d '{"name": "Boucle du lac", "coordinates": [[-0.5516, 47.4739, 22.1], [-0.5525, 47.4737, 20.7]]}' \
-o parcours.gpxcoordinates reprend directement geojson.features[0].geometry.coordinates (2 à 50 000 points). name (120 caractères) et description (500 caractères) sont facultatifs.
/v1/gpx
Turns a route into a GPX 1.1 file, to import into a watch or running app. Does not count towards the quota.
curl -X POST {{BASE}}/v1/gpx \
-H "X-API-Key: dplus_votre_cle" -H "Content-Type: application/json" \
-d '{"name": "Lake loop", "coordinates": [[-0.5516, 47.4739, 22.1], [-0.5525, 47.4737, 20.7]]}' \
-o route.gpxcoordinates is simply geojson.features[0].geometry.coordinates (2 to 50,000 points). name (120 characters) and description (500 characters) are optional.
/v1/assistant
Un message en langage naturel à l'assistant IA, qui le traduit en parcours calculés sur la carte. Disponible si l'assistant est inclus dans votre accès ; chaque conversation compte une fois dans votre quota, à son premier message abouti ; les messages suivants de la même conversation (session_id) sont inclus, dans la limite de 12.
- Premier message :
message,context(mêmes champs que/v1/routes: départ, rayon, terrain, objectif éventuel) etplace(nom du départ). - Messages suivants :
messageet lesession_idde la réponse précédente (« plus court », « moins de route »). Une conversation expire après 2 heures sans message. - Réponse : soit des parcours dans
routes(même format que/v1/routes) avec un textereply, soit unequestionà poser à votre utilisateur, avec desoptionsde réponse.
curl -X POST {{BASE}}/v1/assistant \
-H "X-API-Key: dplus_votre_cle" -H "Content-Type: application/json" \
-d '{
"message": "Deux fois le tour du lac de Maine",
"place": "Angers",
"context": { "lat": 47.4739, "lon": -0.5516, "radius_km": 8, "terrain": "trail",
"mode": "distance", "target_distance_km": 12 }
}'{
"session_id": "9f2c…",
"reply": "Voici deux tours du lac de Maine…",
"question": null,
"options": [],
"routes": { "routes": [ … ] }
}Les en-têtes X-Assistant-Quota-Limit et X-Assistant-Quota-Remaining indiquent où vous en êtes. Prévoyez un délai d'attente de 180 secondes : l'assistant enchaîne plusieurs calculs.
/v1/assistant
A plain-language message to the AI assistant, which turns it into routes computed on the map. Available when the assistant is included in your access; each conversation counts once towards your quota, on its first successful message; follow-up messages in the same conversation (session_id) are included, up to 12.
- First message:
message,context(same fields as/v1/routes: start, radius, terrain, optional goal) andplace(name of the start). - Follow-up messages:
messageand thesession_idfrom the previous response (“shorter”, “less road”). A conversation expires after 2 hours without a message. - Response: either routes in
routes(same format as/v1/routes) with areplytext, or aquestionto ask your user, with answeroptions.
curl -X POST {{BASE}}/v1/assistant \
-H "X-API-Key: dplus_votre_cle" -H "Content-Type: application/json" \
-d '{
"message": "Twice around Lac de Maine",
"place": "Angers",
"context": { "lat": 47.4739, "lon": -0.5516, "radius_km": 8, "terrain": "trail",
"mode": "distance", "target_distance_km": 12 }
}'{
"session_id": "9f2c…",
"reply": "Here are two laps of Lac de Maine…",
"question": null,
"options": [],
"routes": { "routes": [ … ] }
}The X-Assistant-Quota-Limit and X-Assistant-Quota-Remaining headers tell you where you stand. Set a timeout of 180 seconds: the assistant chains several computations. Send Accept-Language: en to get answers in English.
/v1/usage
Vos deux quotas, ce qu'il en reste (routes et assistant), la date de fin d'un accès de démonstration (expires_on) et le détail par jour sur les 31 derniers jours.
{
"client": "Mon application",
"monthly_quota": 1000,
"used_this_month": 13,
"remaining": 987,
"daily": [ … ]
}/v1/usage
Your two quotas, what is left of them (routes and assistant), the end date of a demo access (expires_on) and a daily breakdown over the last 31 days.
{
"client": "My app",
"monthly_quota": 1000,
"used_this_month": 13,
"remaining": 987,
"daily": [ … ]
}Erreurs
En cas d'erreur, le corps JSON contient un champ detail qui l'explique (en anglais avec l'en-tête Accept-Language: en) ; pour les codes 422, il peut être affiché tel quel à vos utilisateurs.
| Code | Signification | Que faire |
|---|---|---|
| 401 | Clé absente ou inconnue. | Vérifiez l'en-tête X-API-Key. |
| 403 | Clé désactivée ou arrivée à sa date de fin, ou assistant non inclus. | Contactez-nous. |
| 422 | Paramètres invalides, lieu hors couverture ou objectif irréalisable. | Lisez detail ; élargissez le rayon ou changez d'objectif. |
| 429 | Débit par minute dépassé, ou quota mensuel atteint. | Respectez Retry-After ; pour le quota, contactez-nous. |
| 500 | Erreur interne. | Réessayez plus tard ; si elle persiste, signalez-la. |
| 503 | Moteur de calcul momentanément indisponible. | Réessayez après quelques secondes. |
Les générations en erreur ne sont pas décomptées du quota.
Errors
On error, the JSON body contains a detail field explaining it (in English with the Accept-Language: en header); for 422 codes you can show it to your users as is.
| Code | Meaning | What to do |
|---|---|---|
| 401 | Missing or unknown key. | Check the X-API-Key header. |
| 403 | Key disabled or past its end date, or assistant not included. | Contact us. |
| 422 | Invalid parameters, place outside coverage or unreachable goal. | Read detail; widen the radius or change the goal. |
| 429 | Per-minute rate exceeded, or monthly quota reached. | Honour Retry-After; for the quota, contact us. |
| 500 | Internal error. | Try again later; if it persists, report it. |
| 503 | Routing engine temporarily unavailable. | Try again after a few seconds. |
Failed generations do not count towards the quota.
Couverture et limites
- Zone couverte : France métropolitaine. Un point hors de la zone renvoie une erreur 422 explicite.
- Distance : de 1 à 200 km ; dénivelé visé : de 50 à 10 000 m ; rayon : de 1 à 30 km.
- Terrain : une préférence, pas un filtre. Un parcours « route » en pleine forêt ou « trail » en centre-ville emprunte le meilleur réseau disponible ; le
messageindique la part de chemins obtenue. - Dénivelé : calculé sur le modèle de relief de l'IGN. Il peut différer de 10 à 20 % de celui mesuré par une montre à altimètre barométrique.
- Langue : messages et erreurs en français par défaut, en anglais avec l'en-tête
Accept-Language: en.
Coverage and limits
- Coverage: mainland France. A point outside the area returns an explicit 422 error.
- Distance: 1 to 200 km; target elevation gain: 50 to 10,000 m; radius: 1 to 30 km.
- Terrain: a preference, not a filter. A “road” route deep in a forest or a “trail” route in a city centre uses the best network available;
messagegives the resulting share of paths. - Elevation gain: computed on the IGN terrain model. It may differ by 10 to 20% from what a watch with a barometric altimeter measures.
- Language: messages and errors are in French by default, in English with the
Accept-Language: enheader.
Bonnes pratiques
- Appelez l'API depuis votre serveur, jamais directement depuis l'application de vos utilisateurs.
- Gardez en cache les parcours déjà calculés pour un même départ et un même objectif : ils ne changent pas d'un appel à l'autre.
- Pour proposer « d'autres parcours », renvoyez la même requête avec
variantincrémenté et les tracés déjà affichés dansexclude_routes. - Affichez l'attribution « © contributeurs OpenStreetMap » à côté des parcours (licence ODbL des données cartographiques).
- Le schéma complet est disponible au format OpenAPI : documentation interactive.
Prêt à intégrer D+ ?
Décrivez votre projet, nous vous envoyons une clé de test.
Best practices
- Call the API from your server, never directly from your users' app.
- Cache routes already computed for the same start and goal: they do not change from one call to the next.
- To offer “more routes”, send the same request again with
variantincremented and the routes already shown inexclude_routes. - Show the “© OpenStreetMap contributors” attribution next to the routes (ODbL licence of the map data).
- The full schema is available in OpenAPI format: interactive documentation.
Ready to integrate D+?
Describe your project and we will send you a test key.