The /v1/places/validate endpoint takes the country, state, city and postal code a user typed and tells you whether that combination actually exists — component by component — returning the canonical spelling and, when something is off, the closest correction.
It is the last step before you store a shipping address: the autocomplete guides the user, this confirms what they ended up with, including addresses typed by hand or imported from elsewhere.
GET https://api.countrydataapi.com/v1/places/validate
POST https://api.countrydataapi.com/v1/places/validate
Both accept the same fields. GET is a CORS-simple request, so it works from the browser without a preflight; POST takes them in a JSON body.
| Parameter | Type | Required | Description |
|---|---|---|---|
apikey |
string | Yes | Your API authentication key |
country |
string | Yes | Internal id, ISO-2, ISO-3 or name |
state |
string | No | State or province name |
city |
string | No | City name |
zipcode |
string | No | Postal code |
lang |
string | No | Language of the returned names. Defaults to en |
Components you do not send come back with the verdict not_provided — nothing you omit can make the address invalid.
curl "https://api.countrydataapi.com/v1/places/validate?apikey=your-api-key&country=ES&state=Madrid&city=Madrid&zipcode=28001"
const response = await fetch(
'https://api.countrydataapi.com/v1/places/validate',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apikey: 'your-api-key',
country: 'ES',
state: 'Madrid',
city: 'Madrid',
zipcode: '28001',
}),
}
);
const { result } = await response.json();
const { result } = await api.places.validate({
country: 'ES',
zipcode: '28001',
});
result.normalized.state?.name; // "Comunidad de Madrid"
{
"success": true,
"result": {
"valid": true,
"has_corrections": false,
"verdict": {
"country": "confirmed",
"state": "confirmed",
"city": "confirmed",
"zipcode": "confirmed"
},
"normalized": {
"country": {
"id": "66c7a6c9e4bda21f4ab10ef2",
"name": "Spain",
"iso2": "ES",
"iso3": "ESP",
"phone_code": "+34",
"flag": "🇪🇸"
},
"state": { "id": "66c7a6c9e4bda21f4ab10a22", "name": "Comunidad de Madrid" },
"city": { "id": "66c7a6c9e4bda21f4ab1a0f1", "name": "Madrid" },
"zipcode": "28001"
},
"corrections": [],
"postal": {
"format": "#####",
"regex": "^\\d{5}$",
"example": "12345",
"matches_format": true
}
},
"tokens_used": 1,
"remaining_tokens": 4870
}
| Verdict | Meaning |
|---|---|
confirmed |
The value exists and matches exactly what was sent |
corrected |
The component was resolved, but not from what was sent — see corrections |
unconfirmed |
The value could not be verified |
not_provided |
You did not send this component |
valid is true only when every component you sent came back confirmed. Use has_corrections to decide whether to show the user a "did you mean" prompt rather than an error.
The common checkout case: you only asked for a postcode and a city.
curl "https://api.countrydataapi.com/v1/places/validate?apikey=your-api-key&country=ES&zipcode=28001"
The response fills in normalized.state from the postcode, so you can populate the province field for the user instead of asking for it. The same works the other way around: send a city and the state comes back resolved.
Postal code coverage is not complete for every country. If a code is not listed but does match the country's official format, the verdict is corrected rather than unconfirmed, and postal.matches_format is true. Treat that as "plausible, accept it" unless your use case demands certainty.
matches_format is null when the country has no official pattern on record.
{
"valid": false,
"has_corrections": true,
"verdict": { "country": "confirmed", "state": "unconfirmed", "city": "confirmed", "zipcode": "not_provided" },
"corrections": [
{ "component": "state", "input": "Cataluna", "suggestion": "Cataluña" }
]
}
This endpoint consumes 1 token per request, regardless of how many components you send.