# Introduction

> Version générique : tous les champs sont listés avec la formule minimale qui y donne accès (`FREE`, `PRO`, `PARTNER`). Envoyez votre clé API (`Authorization: Bearer th_live_…`) ou connectez-vous à votre espace développeur pour obtenir la version personnalisée.

L'API Trail Horizon donne accès au catalogue des courses de trail publiées sur [Trail Horizon](https://trailhorizon.fr), en lecture seule et au format JSON.

- **Base URL** : `https://trailhorizon.fr/api/v1`
- **Format** : JSON, UTF-8. Les dates sont au format ISO 8601.
- **Version** : `/v1` est stable. Ajouter un champ n'est pas un changement cassant : ignorez les champs inconnus. Un changement cassant (renommage, suppression, changement de type, nouvelle valeur d'une liste fermée) donnera lieu à `/v2`.

## Authentification

Envoyez votre clé API dans le header `Authorization` de chaque requête. Les clés de production ont la forme `th_live_` suivie de 32 caractères (les clés de test, `th_test_`, sont décrites dans [Domaines autorisés](domains.md)). Elles se créent dans votre espace développeur et ne sont affichées qu'une fois.

Envoyez aussi, sur **chaque** appel, le header `X-TrailHorizon-Origin` : le domaine du site qui exploite les données, parmi vos [domaines autorisés](domains.md). Sans ce header, l'API répond `403 origin_required`.

```bash
curl -H "Authorization: Bearer th_live_VOTRE_CLE" \
  -H "X-TrailHorizon-Origin: votre-domaine.fr" \
  "https://trailhorizon.fr/api/v1/races"
```

**API serveur à serveur** : appelez-la depuis votre backend, jamais depuis un navigateur. Elle n'envoie aucun en-tête CORS (les appels depuis une page web sont bloqués) et une clé placée dans du code client serait exposée à tous.

### Conditions d'utilisation et délai de grâce

Quand une nouvelle version des conditions d'utilisation est publiée (version actuelle : `2026-11-03`), votre accès continue de fonctionner pendant un **délai de grâce de 30 jours** à partir de la date de la version. Pendant ce délai, chaque réponse porte l'en-tête `X-TrailHorizon-Terms: update-required; deadline=<date ISO 8601>` : acceptez les nouvelles conditions depuis votre espace développeur avant cette date. Passé le délai, l'API répond `403 tos_outdated` jusqu'à l'acceptation. Un compte qui n'a jamais accepté les conditions reçoit `403 tos_not_accepted` immédiatement, sans délai. Dans ces deux cas, les envois de webhooks sont mis en attente et reprennent dès l'acceptation des conditions.

## Endpoints

| Endpoint | Formules | Description | Documentation |
| --- | --- | --- | --- |
| `GET /races` | `FREE`, `PRO`, `PARTNER` | Liste des courses publiées, paginée par curseur. | [Détail](races.md) |
| `GET /races/{id}` | `FREE`, `PRO`, `PARTNER` | Une course publiée. | [Détail](race.md) |
| `GET /races/{id}/editions` | `PARTNER` | Historique des éditions par année et par format. | [Détail](editions.md) |
| `GET /export` | `PARTNER` | Catalogue complet en un seul document JSON. | [Détail](export.md) |

## Réponses et champs

Seuls les champs autorisés pour votre compte sont renvoyés. Un champ non autorisé est **absent** de la réponse (jamais `null`). Un champ `null` signifie que la donnée n'est pas renseignée. Voir le [catalogue des champs](fields.md).

## Quotas

Les quotas sont comptés **par compte** (toutes clés de production confondues). Les clés de test (`th_test_`) ont leurs propres compteurs, séparés de ceux de la production. Valeurs des formules publiques ; la formule `PARTNER` est définie par contrat.

| Limite | Free | Pro |
| --- | --- | --- |
| Licence | Non commerciale | Commerciale |
| Requêtes par jour | 500 | 10 000 |
| Requêtes par minute | 20 | 60 |
| Taille de page maximale | 100 | 100 |
| Export complet | Non disponible | Non disponible |
| Historique des éditions | Non | Non |
| Webhooks | Non | Oui (2 endpoint(s) maximum) |
| Clés API de production | 2 maximum | 5 maximum |
| Domaines autorisés | 1 maximum | 5 maximum |
| Clés de test | 200 requêtes par jour, 10 par minute | 200 requêtes par jour, 10 par minute |
| Membres du compte | 1 maximum | 5 maximum |

### En-têtes de limitation

Chaque réponse porte les en-têtes suivants (le plus contraignant de la minute et du jour), à l'exception des refus d'origine (`origin_*`), qui ne consomment pas de quota et ne portent pas ces en-têtes. Avec une clé de test, ils décrivent le quota de test :

| En-tête | Description |
| --- | --- |
| `X-RateLimit-Limit` | Limite de la fenêtre. |
| `X-RateLimit-Remaining` | Requêtes restantes dans la fenêtre. |
| `X-RateLimit-Reset` | Instant de remise à zéro (timestamp Unix, en secondes). |
| `Retry-After` | Sur un `429` : délai en secondes avant de réessayer. |

Les requêtes refusées par un quota ne sont pas décomptées. Un refus d'origine (`origin_*`) ne l'est pas non plus : il est journalisé et compté dans vos erreurs du jour. Les autres erreurs (`4xx`) sont, elles, comptées comme des requêtes.

## Erreurs

Toutes les erreurs ont la même enveloppe :

```json
{
  "error": {
    "code": "invalid_parameter",
    "message": "Invalid \"limit\": must be between 1 and 100."
  }
}
```

| Statut | Code | Signification |
| --- | --- | --- |
| 401 | `missing_api_key` | Header `Authorization` absent. |
| 401 | `invalid_api_key` | Clé mal formée ou inconnue. |
| 401 | `api_key_revoked` | Clé révoquée. |
| 403 | `account_suspended` / `account_revoked` | Compte suspendu ou révoqué. |
| 403 | `tos_not_accepted` | Conditions d'utilisation de l'API jamais acceptées (refus immédiat). |
| 403 | `tos_outdated` | Nouvelle version des conditions non acceptée dans le délai de grâce. |
| 403 | `origin_required` | Header `X-TrailHorizon-Origin` absent. Voir [Domaines autorisés](domains.md). |
| 403 | `origin_not_allowed` | Origine invalide, ou qui ne correspond à aucun domaine autorisé rattaché à la clé. |
| 403 | `origin_suspended` | Domaine suspendu (au-delà de la limite de votre formule) ou déclaré Non conforme après revue. |
| 403 | `forbidden` / `field_not_allowed` | Endpoint ou filtre non autorisé pour votre formule. |
| 404 | `not_found` | Course inconnue, non publiée ou masquée. |
| 422 | `invalid_parameter` | Paramètre invalide. |
| 429 | `rate_limit_exceeded` | Trop de requêtes dans la minute. Voir `Retry-After`. |
| 429 | `daily_quota_exceeded` | Quota journalier atteint. Voir `Retry-After`. |
| 429 | `export_quota_exceeded` | Nombre d'exports journaliers atteint. |
| 500 | `internal_error` | Erreur interne. |

## Suivre les mises à jour

Relisez le catalogue à intervalle régulier (`GET /races` paginé, ou `GET /export`). Le champ `updatedAt` de chaque course (s'il est disponible pour votre compte) permet d'ignorer ce qui n'a pas changé. Les comptes disposant des [webhooks](webhooks.md) sont notifiés à chaque changement.

## Licence et attribution

Votre usage des données est encadré par les [conditions d'utilisation de l'API](https://trailhorizon.fr/legal/api-terms) (version 2026-11-03) : licence non commerciale (`FREE`), commerciale (`PRO`) ou par contrat (`PARTNER`), revente des données interdite.

**Citation obligatoire** : chaque course affichée doit porter la mention « Données : Trail Horizon » avec un lien, **sans `nofollow`**, vers l'URL `url` de la course, **reprise telle quelle, paramètres de requête compris** (les paramètres `utm_*` permettent à Trail Horizon d'identifier votre trafic : ne les retirez pas et n'en ajoutez pas).

```html
Données : <a href="https://trailhorizon.fr/races/…?utm_source=partner-votre-slug&amp;utm_medium=api&amp;utm_campaign=race-data">Trail Horizon</a>
```

## Pages de la documentation

- [Domaines autorisés](domains.md)
- [GET /races](races.md)
- [GET /races/{id}](race.md)
- [GET /races/{id}/editions](editions.md)
- [GET /export](export.md)
- [Webhooks](webhooks.md)
- [Catalogue des champs](fields.md)
