Speed limits, looked up in milliseconds.
SpeedMap is a lookup API that returns the speed limit for any coordinate across the UK, Northern Ireland and the Republic of Ireland. Two endpoints. One clean JSON response.
From zero to first lookup
Four steps to your first response. No SDKs to install — SpeedMap is a plain HTTPS/JSON API.
Authenticate with your bearer token
Every request needs a bearer token, issued to authorised customers under your service agreement. Send it on every call.
Authorization: Bearer <your_token>
Call the Speed endpoint
Pass a single latitude/longitude pair to get the speed limit at that point. The production base URL is https://api.iw-slu.com.
curl -X GET "https://api.iw-slu.com/Speed?latitude=51.5074&longitude=-0.1278" \ -H "Authorization: Bearer <your_token>"
Read the response
Every response — success or error — comes back in the same envelope: a message, an error, and a data payload.
{
"message": "Success",
"error": null,
"data": {
"id": 184522,
"latitude": 51.5074,
"longitude": -0.1278,
"roadName": "Whitehall",
"speedLimit": 30, // local unit
"distance": 4.2, // metres
"errorMessage": null
}
}
Move to bulk when you need scale
Looking up more than one point? Use POST /Speed/bulk instead of looping single calls — see the endpoint reference below.
Securing your requests
Both endpoints are protected and return nothing without a valid credential. Tokens are issued to authorised customers under the terms of their service agreement.
Authorization: Bearer <your_token>
Send this header on every request. A missing or invalid token returns 401 Unauthorized with a Problem Details error body — see errors & rate limits.
Tokens are issued, rotated, and administered manually by Insight Warehouse — there's no self-serve portal. Expiry and access level are set according to the subscription tier purchased. For a new token, a rotation, or any token issue, contact info@insight-warehouse.co.uk.
Two endpoints, nothing else
SpeedMap is deliberately small in surface area: a single lookup and a batch lookup. Coverage is currently limited to the UK, Northern Ireland and the Republic of Ireland — coordinates outside these territories will not return a match. Prefer to explore the live schema and try calls directly? Use the interactive Swagger UI.
Returns the speed limit for a single coordinate.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| latitude | number | Yes | Between -90 and 90. |
| longitude | number | Yes | Between -180 and 180. |
Response fields (data)
The field that matters is speedLimit — that's the match. Everything else on this object (road name, class, IDs, distance) is supporting context rather than the primary result.
| Field | Type | Description |
|---|---|---|
| id | number | Identifier of the nearest road segment found, whether or not it's within the match buffer. |
| latitude / longitude | number | Coordinates of the matched point. |
| overtureId | string (UUID) | Overture identifier of the nearest road. |
| fClass | string | Functional road class — e.g. motorway, primary, residential. |
| roadName | string | Name of the nearest road. |
| speedLimit | number | The match. Posted speed limit, in the unit used by that country. 0 when the point is outside the match buffer (see below) — not a real zero limit. |
| distance | number | Distance from the queried point to the nearest road, in metres. |
| errorMessage | string | Populated with a human-readable explanation when the point is outside the 20m match buffer; null on a normal match. |
A coordinate is matched to the nearest road within a 20 metre buffer. If the nearest road with a speed limit is further than 20m away, the lookup is treated as a non-match: speedLimit comes back as 0 (not a genuine zero limit — read it as "no valid match"), and errorMessage explains why. The rest of the object (roadName, fClass, distance, etc.) still describes the nearest road found, even though it fell outside the buffer — useful context, but don't treat it as a confirmed match.
Example — normal match
// Request curl 'https://api.iw-slu.com/Speed?latitude=51.5074&longitude=-0.1278' \ -H 'Authorization: Bearer <your_token>'
// Response — 200 OK { "message": "Success", "error": null, "data": { "id": 184522, "longitude": -0.1278, "latitude": 51.5074, "overtureId": "3fa85f64-…", "fClass": "primary", "roadName": "Whitehall", "speedLimit": 30, "distance": 4.2, "errorMessage": null } }
Example — outside the 20m buffer
// Request curl 'https://api.iw-slu.com/Speed?latitude=51.3976&longitude=-0.10382' \ -H 'Authorization: Bearer <your_token>'
// Response — 200 OK, no valid match { "message": "Success", "error": null, "data": { "id": 3375342, "longitude": -0.10382, "latitude": 51.3976, "overtureId": "9e7c7b2f-…", "fClass": "secondary", "roadName": "Brigstock Road", "speedLimit": 0, "distance": 21.01, "errorMessage": "The point requested is more than 20m away from any road in our network." } }
Returns speed limits for a list of coordinate pairs. Results come back paginated — each result uses the same fields as GET /Speed above.
Request body
| Field | Type | Description |
|---|---|---|
| coordinates | array | List of { latitude, longitude } pairs. Required. |
| page | number | Page of results to return. |
| pageSize | number | Results per page. |
| search | string | Optional filter applied to results. |
Pagination fields (response)
| Field | Type | Description |
|---|---|---|
| total | number | Total results across all pages. |
| page / pageSize | number | Echoes the request. |
| totalPages | number | Total number of pages. |
| hasNextPage / hasPreviousPage | boolean | Whether more pages exist either direction. |
Example
// Request curl -X POST 'https://api.iw-slu.com/Speed/bulk' \ -H 'Authorization: Bearer <your_token>' \ -H 'Content-Type: application/json' \ -d '{ "page": 1, "pageSize": 100, "coordinates": [ { "latitude": 51.5074, "longitude": -0.1278 }, { "latitude": 53.4808, "longitude": -2.2426 } ] }'
// Response — 200 OK { "message": "Success", "error": null, "data": [ { "id": 184522, "roadName": "Whitehall", "speedLimit": 30, … }, { "id": 207781, "roadName": "Deansgate", "speedLimit": 20, … } ], "pagination": { "total": 2, "page": 1, "pageSize": 100, "totalPages": 1, "hasNextPage": false, "hasPreviousPage": false } }
500 coordinate pairs is the maximum accepted per /Speed/bulk request. Send more than that and expect a rejection — split larger jobs into multiple requests (see below).
Send coordinates as objects, not tuples
Each entry in the coordinates array needs explicit latitude/longitude keys — not [lat, lon] pairs or a flattened list.
"coordinates": [
{ "latitude": 51.5074, "longitude": -0.1278 },
{ "latitude": 53.4808, "longitude": -2.2426 },
{ "latitude": 55.9533, "longitude": -3.1883 }
]
Chunk large jobs into batches of 500
500 pairs is the maximum accepted per request — confirmed as a hard limit, not just a recommendation. For anything bigger, split into batches rather than sending one large array.
// Simple chunking, JS function chunk(coords, size = 500) { const out = []; for (let i = 0; i < coords.length; i += size) { out.push(coords.slice(i, i + size)); } return out; }
Always check hasNextPage
The bulk endpoint paginates its response independently of how many coordinates you sent. If you set a pageSize smaller than your batch, or the API applies its own cap, you won't get everything back in one call — loop on pagination.hasNextPage until it's false, incrementing page each time.
What can go wrong, and what it costs
One thing worth knowing up front: you are not charged for requests that return no match or a server error. Every error response — 4xx or 5xx — uses the same Problem Details format (RFC 7807).
| Status | Meaning |
|---|---|
| 200 | Lookup successful. Standard envelope with the data object above. |
| 400 | Bad request — missing or out-of-range coordinates. |
| 401 | Missing or invalid bearer token. |
| 429 | Rate limit exceeded. Applies per account, on both Speed and Speed/bulk — the mechanism is real and was triggered during performance testing. Confirmed: there is currently no fixed requests-per-minute or per-day threshold defined. |
| 500 | Unexpected server error — not billed. |
Problem Details error body
Every error response — 4xx or 5xx — comes back in this shape (RFC 7807).
// Example — 401 Unauthorized { "type": "https://api.iw-slu.com/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Missing or invalid credentials.", "instance": "/Speed" }
Common questions
Am I charged for a coordinate that has no matching speed limit?
No. No-match results and server errors are not billed.
What's the largest batch I can send to /Speed/bulk?
500 coordinate pairs — confirmed as the maximum per request. For very large jobs, split into multiple requests.
What happens if I exceed the rate limit?
You'll receive a 429 Too Many Requests response with a Problem Details error body. Rate limiting applies per account, on both endpoints — but there's currently no fixed requests-per-minute or per-day threshold defined.
What does it mean if errorMessage isn't null?
The point is more than 20 metres from the nearest road with a speed limit — confirmed as the match buffer. speedLimit comes back as 0 in this case (not a real zero limit) and errorMessage explains the distance. The call still returns 200 OK; check errorMessage on the data object rather than the HTTP status.
Does the API cover live or temporary speed restrictions (e.g. roadworks)?
No. SpeedMap API returns permanent speed limits.
