API Documentation

HotelGrade provides official hotel star ratings sourced directly from national tourism authorities. Every rating is government-issued — no scraped data, no review aggregates.

Base URL: https://hotelgrade.eu — All API endpoints are relative to this URL.

What's included

The API gives you access to 24,000+ hotels across France, Portugal, Spain (12 regions), and more countries coming. Each record includes the official star classification, GPS coordinates (where available), contact details, and the source authority.

Quick Start

Get up and running in under 2 minutes.

1

Create a free account

Go to hotelgrade.eu/login.html and register. You get an API key instantly — no credit card required.

2

Copy your API key

Your key is shown once after registration and is always visible in your dashboard. It looks like hsa_a1b2c3d4...

3

Make your first request

Pass your key in the X-API-Key header on every request.

# Search for 5-star hotels in France
curl "https://hotelgrade.eu/api/hotels?country=France&stars=5&limit=10" \
  -H "X-API-Key: hsa_your_key_here"
const response = await fetch(
  'https://hotelgrade.eu/api/hotels?country=France&stars=5&limit=10',
  { headers: { 'X-API-Key': 'hsa_your_key_here' } }
);
const data = await response.json();
console.log(data.data); // array of hotels
import requests

response = requests.get(
    'https://hotelgrade.eu/api/hotels',
    params={'country': 'France', 'stars': 5, 'limit': 10},
    headers={'X-API-Key': 'hsa_your_key_here'}
)
data = response.json()
print(data['data'])

Authentication

All API requests require a valid API key passed in the X-API-Key request header.

HTTP Header
X-API-Key: hsa_your_key_here

API keys follow the format hsa_ followed by 48 hexadecimal characters. You can generate up to 3 keys per account from your dashboard.

Keep your API key secret. Do not expose it in client-side JavaScript, public repositories, or logs. If a key is compromised, revoke it immediately from your dashboard.

Try the API without a key

The /api/demo endpoint is publicly accessible without authentication and returns up to 5 results. It's rate-limited to 10 requests per minute per IP.

cURL
curl "https://hotelgrade.eu/api/demo?country=Portugal&stars=5"

API Key Management

Manage your keys from the dashboard. You can also use the REST endpoints below directly.

POST /auth/register

Create a new account. Returns a JWT token, user info, and your first API key.

Request body

FieldTypeDescription
email requiredstringYour email address
password requiredstringMin. 8 characters
Response 201
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI...",  // JWT — valid 30 days
  "user": { "email": "you@example.com" },
  "api_key": "hsa_a1b2c3d4..."  // shown once — save it!
}
POST /auth/login

Authenticate and get a fresh JWT token.

Request body

FieldTypeDescription
email requiredstring
password requiredstring
Response 200
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI...",
  "user": { "email": "you@example.com" }
}
GET /api/dashboard/me

Returns your account info and all API keys with usage stats. Requires a JWT token in the Authorization header.

Pass the JWT from login/register as Authorization: Bearer <token> — not your API key.

Response 200
{
  "success": true,
  "user": { "email": "you@example.com" },
  "keys": [
    {
      "id": 12,
      "name": "My App",
      "plan": "free",
      "status": "active",
      "requests_today": 47,
      "requests_total": 1203,
      "last_used_at": "2026-03-23T10:42:00Z",
      "created_at": "2026-01-15T08:00:00Z"
    }
  ],
  "can_create_key": true  // false when 3 keys already exist
}
POST /api/dashboard/keys

Generate a new API key (max 3 per account). Requires JWT.

Request body

FieldTypeDescription
name optionalstringLabel for this key (e.g. "Production")
DELETE /api/dashboard/keys/:id

Revoke an API key permanently. The key stops working immediately. Requires JWT.

Revocation is permanent. Any application still using the revoked key will receive 403 Forbidden.

Search Hotels

GET /api/hotels

Search and filter hotels across the entire database. Returns paginated results.

Query parameters

ParameterTypeDescription
country optionalstringFilter by country, case-insensitive. e.g. France, Spain, Portugal
city optionalstringPartial city name match. e.g. Paris
stars optionalintegerFilter by star rating: 1 to 5
name optionalstringPartial hotel name search. e.g. Hilton
page optionalintegerPage number, default 1
limit optionalintegerResults per page, default 20. Max depends on plan (Free: 20, Starter: 50, Pro: 100)
curl "https://hotelgrade.eu/api/hotels?country=Spain&stars=4&limit=20" \
  -H "X-API-Key: hsa_your_key_here"
const params = new URLSearchParams({ country: 'Spain', stars: 4, limit: 20 });
const res = await fetch(`https://hotelgrade.eu/api/hotels?${params}`, {
  headers: { 'X-API-Key': 'hsa_your_key_here' }
});
const { data, pagination } = await res.json();
r = requests.get('https://hotelgrade.eu/api/hotels',
    params={'country': 'Spain', 'stars': 4, 'limit': 20},
    headers={'X-API-Key': 'hsa_your_key_here'})

Response

JSON
{
  "success": true,
  "data": [
    {
      "id": 18234,
      "name": "Hotel Arts Barcelona",
      "stars": 5,
      "country": "Spain",
      "country_code": "ES",
      "city": "Barcelona",
      "address": "Carrer de la Marina, 19-21",
      "latitude": 41.3874,
      "longitude": 2.1965,
      "phone": "+34 93 221 10 00",
      "website": "https://www.hotelartsbarcelona.com",
      "email": null,
      "source": "spain-oficial",
      "source_id": "es-ct-HO-000123"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 487,
    "totalPages": 25
  }
}

Rate limit headers

Every response includes headers indicating your current rate limit status:

Response Headers
X-RateLimit-Limit: 10             # requests allowed per minute
X-RateLimit-Remaining: 8          # requests left this minute
X-RateLimit-Reset: 1711187120    # Unix timestamp of window reset
X-RateLimit-Daily-Limit: 100      # daily quota
X-RateLimit-Daily-Remaining: 53  # daily quota remaining

Get Hotel by ID

GET /api/hotels/:id

Retrieve full details for a single hotel using its internal ID from a search result.

Path parameters

ParameterTypeDescription
id requiredintegerHotel ID returned in search results
cURL
curl "https://hotelgrade.eu/api/hotels/18234" \
  -H "X-API-Key: hsa_your_key_here"
Response 200
{
  "success": true,
  "data": { /* full hotel object */ }
}

Countries Statistics

GET /api/stats/countries

Returns hotel count and star rating statistics per country. Useful for understanding coverage and market analysis.

cURL
curl "https://hotelgrade.eu/api/stats/countries" \
  -H "X-API-Key: hsa_your_key_here"
Response 200
{
  "success": true,
  "data": [
    {
      "country": "France",
      "total_hotels": 13209,
      "avg_stars": "3.2",
      "min_stars": 1,
      "max_stars": 5,
      "hotels_with_stars": 12847
    },
    // ...
  ]
}

Stars Distribution

GET /api/stats/stars

Count of hotels by star rating. Optionally filter by country.

Query parameters

ParameterTypeDescription
country optionalstringFilter by country
cURL
curl "https://hotelgrade.eu/api/stats/stars?country=France" \
  -H "X-API-Key: hsa_your_key_here"
Response 200
{
  "success": true,
  "data": [
    { "stars": 1, "count": "512" },
    { "stars": 2, "count": "1840" },
    { "stars": 3, "count": "4203" },
    { "stars": 4, "count": "4918" },
    { "stars": 5, "count": "1736" }
  ]
}

Countries List

GET /api/stats/countries/list

Returns all countries currently available in the database. Use this to populate a country picker in your UI.

Response 200
{
  "success": true,
  "data": ["France", "Portugal", "Spain"]
}

Hotel Object Reference

Every hotel endpoint returns objects with the following fields:

FieldTypeDescription
idintegerInternal unique identifier
namestringOfficial hotel name
starsinteger | nullOfficial star rating 1–5, or null if not classified
countrystringCountry name (e.g. France)
country_codestringISO 3166-1 alpha-2 (e.g. FR)
citystring | nullCity name
addressstring | nullFull street address
latitudefloat | nullGPS latitude (WGS84)
longitudefloat | nullGPS longitude (WGS84)
phonestring | nullContact phone number
websitestring | nullHotel website URL
emailstring | nullContact email
sourcestringData source identifier (see below)
source_idstringOriginal ID in the source registry
created_atISO 8601First ingestion timestamp
updated_atISO 8601Last update timestamp

Source identifiers

SourceAuthorityPriority
atout-franceAtout France — French Ministry of Tourism100 (highest)
turismo-portugalTurismo de Portugal — RNET registry100
spain-oficialSpanish regional tourism authorities90
datatourismeDATAtourisme — French open data platform70
overpassOpenStreetMap community10 (lowest)

When the same hotel exists in multiple sources, the highest-priority version is kept. Official government data always wins over community data.

Plans & Rate Limits

Rate limits apply per API key. When a limit is exceeded the API responds with HTTP 429.

Free
$0/mo
  • 100 requests / day
  • 10 requests / minute
  • 20 results per page
  • Up to 3 API keys
Pro
$29/mo
  • 50,000 requests / day
  • 200 requests / minute
  • 100 results per page
  • Up to 3 API keys
Enterprise
Custom
  • Unlimited requests / day
  • 1,000 requests / minute
  • 100 results per page
  • SLA + dedicated support

Daily quotas reset at 00:00 UTC. The resets_at field in the 429 response gives the exact timestamp of the next reset.

Error Codes

All error responses follow a consistent JSON format:

Error Response
{
  "success": false,
  "error": "Human-readable description of the error."
}
200Success
201Resource created (register, create key)
400Bad request — invalid or missing parameter
401Missing or invalid API key / JWT token
403API key revoked, suspended, or expired
404Hotel not found
409Conflict — email already registered
429Rate limit exceeded — check retry_after or resets_at
500Internal server error — try again or contact support

429 response example

JSON
{
  "success": false,
  "error": "Limite journalière dépassée.",
  "limit": 100,
  "plan": "Free",
  "resets_at": "2026-03-24T00:00:00.000Z",
  "upgrade": "Passez à un plan supérieur pour augmenter votre quota."
}