GeoPin provides a straightforward REST API that lets you submit a photo and receive a predicted location within the Netherlands. Whether you are building an internal verification tool, enriching a content management pipeline, or adding location awareness to a mobile app, integration takes just a few minutes. This guide covers authentication, making your first request, handling responses and best practices for production use.
Getting your API key
Every request to the GeoPin API requires an API key passed in the Authorization header. You can generate a key from your dashboard at geopin.nl/dashboard after creating an account. Free accounts receive 100 requests per month. Paid plans offer higher limits and priority processing.
Keep your API key secret. Never include it in client-side JavaScript shipped to end users. If you suspect a key has been compromised, revoke it immediately from the dashboard and generate a new one.
Making your first request
The core endpoint is POST /api/v1/geolocate. You send an image, and GeoPin returns a ranked list of predicted locations.
curl
curl -X POST https://api.geopin.nl/api/v1/geolocate \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "image=@photo.svg"
Python
import requests
API_KEY = "YOUR_API_KEY"
URL = "https://api.geopin.nl/api/v1/geolocate"
with open("photo.svg", "rb") as f:
response = requests.post(
URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"image": ("photo.svg", f, "image/jpeg")},
)
data = response.json()
print(f"Top prediction: {data['results'][0]['latitude']}, "
f"{data['results'][0]['longitude']}")
print(f"Confidence: {data['results'][0]['confidence']}")
JavaScript (Node.js)
import fs from "fs";
import FormData from "form-data";
import fetch from "node-fetch";
const API_KEY = "YOUR_API_KEY";
const URL = "https://api.geopin.nl/api/v1/geolocate";
const form = new FormData();
form.append("image", fs.createReadStream("photo.svg"));
const response = await fetch(URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
...form.getHeaders(),
},
body: form,
});
const data = await response.json();
console.log("Top prediction:", data.results[0]);
Understanding the response
A successful response returns JSON with the following structure:
{
"request_id": "req_8f3a2b1c",
"status": "success",
"processing_time_ms": 1423,
"results": [
{
"rank": 1,
"latitude": 52.3676,
"longitude": 4.9041,
"confidence": 0.92,
"radius_m": 150,
"region": "Amsterdam",
"province": "Noord-Holland"
},
{
"rank": 2,
"latitude": 52.3712,
"longitude": 4.8952,
"confidence": 0.78,
"radius_m": 300,
"region": "Amsterdam",
"province": "Noord-Holland"
}
]
}
Key fields to note:
confidenceis a float between 0 and 1. Values above 0.85 generally indicate high-confidence predictions. Values below 0.5 suggest the model is uncertain and you should treat the result with caution.radius_mis the estimated accuracy radius in metres. A smaller radius means the model is more spatially precise.resultsalways contains up to five ranked predictions. Returning multiple candidates allows you to apply your own re-ranking logic if your application has additional context, such as a known city or time zone.
Error handling
The API uses standard HTTP status codes. Here are the ones you should explicitly handle:
| Status | Meaning | Action |
|---|---|---|
| 200 | Success | Process the results |
| 400 | Bad request (missing image, unsupported format) | Check your request payload |
| 401 | Invalid or missing API key | Verify your key |
| 413 | Image exceeds 20 MB limit | Resize before uploading |
| 429 | Rate limit exceeded | Wait and retry |
| 500 | Internal server error | Retry with exponential backoff |
For 429 responses, the Retry-After header indicates how many seconds to wait before retrying. A simple retry strategy in Python looks like this:
import time
def geolocate_with_retry(image_path, max_retries=3):
for attempt in range(max_retries):
response = requests.post(
URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"image": open(image_path, "rb")},
)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 5))
time.sleep(wait)
continue
response.raise_for_status()
raise Exception("Max retries exceeded")
Batch processing
If you need to geolocate many images — for example, processing a backlog of archival photos — use the batch endpoint POST /api/v1/geolocate/batch. This accepts up to 50 images in a single request and processes them in parallel on our GPU cluster.
files = [
("images", (f"photo_{i}.svg", open(f"photo_{i}.jpg", "rb"), "image/jpeg"))
for i in range(50)
]
response = requests.post(
"https://api.geopin.nl/api/v1/geolocate/batch",
headers={"Authorization": f"Bearer {API_KEY}"},
files=files,
)
for result in response.json()["results"]:
print(f"{result['filename']}: {result['latitude']}, {result['longitude']}")
Batch requests are billed as one request per image but benefit from reduced overhead and faster total processing time compared to sequential individual calls.
Best practices
Image quality matters. Higher-resolution images with visible landmarks, street signs or distinctive architecture yield better results. Cropped close-ups of faces or generic interior scenes will produce low-confidence results because they lack geographic context.
Strip EXIF data if privacy matters. GeoPin does not read or retain EXIF GPS tags from uploaded images. Our geolocation is purely visual. If your workflow involves sensitive images, strip metadata before uploading as an additional security layer.
Cache results on your end. If you are geolocating the same image more than once, cache the result locally rather than re-submitting. This saves both API quota and processing time.
Use confidence thresholds. Not every image can be reliably geolocated. Build your application logic to handle low-confidence responses gracefully. For example, you might display the result with a disclaimer when confidence is between 0.5 and 0.85, and suppress the result entirely below 0.5.
Use HTTPS only. All API endpoints require TLS. Requests over plain HTTP are rejected. This ensures your API key and image data are encrypted in transit.
Rate limits and pricing
| Plan | Monthly requests | Price |
|---|---|---|
| Trial | 3 | Free |
| Starter | 100 | EUR 19 |
| Pro | 500 | EUR 99 |
| Business | 2,000 | EUR 149 |
All plans include access to the batch endpoint, webhook notifications and full API documentation. Enterprise plans additionally provide dedicated support, custom SLAs and on-premises deployment options.
What is coming next
We are actively developing a WebSocket endpoint for real-time video geolocation and a client SDK for Python and TypeScript that wraps the REST API with built-in retries, connection pooling and type-safe response objects. Follow our changelog at geopin.nl/changelog to stay up to date.
If you run into issues during integration, reach out at support@geopin.nl or open a discussion in our developer forum. We typically respond within a few hours on business days.