curl -H "Authorization: Bearer $GEO_API_KEY" \
  "https://geodata.bitfertig.de/api/v1/geocode?q=Neue+Promenade+5,+10178+Berlin&limit=3"
const res = await fetch(
  "https://geodata.bitfertig.de/api/v1/geocode?" + new URLSearchParams({
    q: "Neue Promenade 5, 10178 Berlin",
    limit: "3",
  }),
  { headers: { Authorization: `Bearer ${process.env.GEO_API_KEY}` } },
);

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status}: ${error?.message ?? "Unbekannter Fehler"}`);
}

const { results } = await res.json();
console.log(results[0].location); // { lat: 52.52297, lon: 13.40171 }
import os
import httpx

client = httpx.Client(
    base_url="https://geodata.bitfertig.de/api/v1",
    headers={"Authorization": f"Bearer {os.environ['GEO_API_KEY']}"},
    timeout=10.0,
)

response = client.get("/geocode", params={"q": "Neue Promenade 5, 10178 Berlin", "limit": 3})
response.raise_for_status()

for place in response.json()["results"]:
    print(place["name"], place["location"])
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.geodata.key'))
    ->baseUrl('https://geodata.bitfertig.de/api/v1')
    ->get('/geocode', ['q' => 'Neue Promenade 5, 10178 Berlin', 'limit' => 3])
    ->throw();

$first = $response->json('results.0');

Reverse-Geocoding

curl -H "Authorization: Bearer $GEO_API_KEY" \
  "https://geodata.bitfertig.de/api/v1/reverse?lat=53.550341&lon=10.000654&limit=3"
async function nearestAddress(lat, lon) {
  const params = new URLSearchParams({
    lat: String(lat),
    lon: String(lon),
    limit: "3",
  });

  const res = await fetch(`https://geodata.bitfertig.de/api/v1/reverse?${params}`, {
    headers: { Authorization: `Bearer ${process.env.GEO_API_KEY}` },
  });

  const { results } = await res.json();

  // Ergebnisse kommen bereits nach Entfernung sortiert
  return results.map((r) => ({
    address: `${r.address.street} ${r.address.house_number}`,
    city: `${r.address.postal_code} ${r.address.city}`,
    distance: r.distance_m,
  }));
}
response = client.get("/reverse", params={
    "lat": 53.550341,
    "lon": 10.000654,
    "limit": 3,
})

for hit in response.json()["results"]:
    print(f"{hit['distance_m']:>5} m  {hit['name']}")
$nearest = Http::withToken(config('services.geodata.key'))
    ->baseUrl('https://geodata.bitfertig.de/api/v1')
    ->get('/reverse', [
        'lat' => 53.550341,
        'lon' => 10.000654,
        'limit' => 3,
    ])
    ->throw()
    ->collect('results');
curl -X POST "https://geodata.bitfertig.de/api/v1/geocode/batch" \
  -H "Authorization: Bearer $GEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"queries":["Neue Promenade 5, 10178 Berlin","Jakobikirchhof 21, 20095 Hamburg"],"limit":1}'
const res = await fetch("https://geodata.bitfertig.de/api/v1/geocode/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    queries: addresses.slice(0, 50), // max. 50 pro Aufruf
    limit: 1,
  }),
});

const { items } = await res.json();
# Adressen in Blöcke von 50 aufteilen – ein Aufruf kostet 5 Einheiten
def chunks(seq, size=50):
    for i in range(0, len(seq), size):
        yield seq[i:i + size]

for batch in chunks(addresses):
    response = client.post("/geocode/batch", json={"queries": batch, "limit": 1})
    response.raise_for_status()

    for item in response.json()["items"]:
        hit = item["results"][0] if item["results"] else None
        print(item["query"], "→", hit["location"] if hit else "kein Treffer")
foreach (collect($addresses)->chunk(50) as $chunk) {
    $items = Http::withToken(config('services.geodata.key'))
        ->baseUrl('https://geodata.bitfertig.de/api/v1')
        ->post('/geocode/batch', [
            'queries' => $chunk->values()->all(),
            'limit' => 1,
        ])
        ->throw()
        ->collect('items');
}

Kontingent prüfen

curl -H "Authorization: Bearer $GEO_API_KEY" "https://geodata.bitfertig.de/api/v1/me"
const res = await fetch("https://geodata.bitfertig.de/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.GEO_API_KEY}` },
});

const { quota } = await res.json();

if (quota.remaining / quota.limit < 0.1) {
  console.warn(`Nur noch ${quota.remaining} Einheiten bis ${quota.resets_at}`);
}
quota = client.get("/me").json()["quota"]

if quota["remaining"] < 1000:
    raise RuntimeError(f"Kontingent knapp: {quota['remaining']} von {quota['limit']}")
$quota = Http::withToken(config('services.geodata.key'))
    ->baseUrl('https://geodata.bitfertig.de/api/v1')
    ->get('/me')
    ->throw()
    ->json('quota');

// Alternativ ohne Extra-Request: Header jeder Antwort auswerten
// $response->header('X-Quota-Remaining')

Einsatz mit KI

Über den MCP-Server entfällt der Code komplett – die KI ruft die Werkzeuge selbst auf. Wer die API stattdessen als eigenes Tool in einen Agenten einbaut, kann diese Werkzeugdefinition übernehmen:

{
  "name": "geocode",
  "description": "Löst eine Adresse, PLZ oder einen Ortsnamen in Koordinaten auf. Mit Hausnummer wird die exakte Position geliefert, sonst die Straßen- bzw. Ortsmitte. Der Bestand enthält Adressen, keine Points of Interest.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "country": { "type": "string", "description": "ISO 3166-1 alpha-2, z. B. DE" },
      "limit": { "type": "integer", "default": 5 }
    },
    "required": ["query"]
  }
}

Jeder Treffer nennt in type seine Genauigkeit (address, street oder city) – für einen Agenten ist das der Hinweis, ob eine Angabe präzise genug war. Genau so arbeitet auch der MCP-Server.