Routes

The API is served at https://i18n-rules.nextcommerce.com. It is public and GET-only, with access-control-allow-origin: *, and has no key: everything it serves is reference data or the caller's own location.

RouteDescription
GET /v1/geoWhere the visitor is, and optionally the rules
GET /v1/countriesThe country list
GET /v1/countries/:countryOne country's rules
GET /v1/countries/:country/statesA country's state options
GET /v1/locales/:langEvery text in one language
GET /v1/flags/:code.svgA country's flag
POST /v1/validateCheck what a shopper typed
GET /v1/versionThe data version
GET /v1/healthWhether the Worker is up

Query parameters

ParameterDescription
langThe language texts are in
countriesThe countries a storefront ships to
includerules and states, comma-separated
countryOn /v1/geo, the country to give rules for

lang is any of da de en es fi fr it nl no pt sv th, with or without a region (en-US); nb and nn are served no. Without it the language comes from Accept-Language, read in preference order (ja,es;q=0.9 is Spanish), then English. See All languages.

countries is a comma-separated list of codes, US,PR,GU. It narrows the country list, and on /v1/geo decides which country a form opens on. Unknown codes are dropped rather than refused, so one stale entry in a storefront's settings narrows the list instead of breaking the form.

How a cold form loads

A checkout page does not know which country's rules it needs until it knows where the visitor is, so that is the one request that depends on the visitor, and it carries the rules with it. The other two are the same for everyone, so the client sends all three at once and the browser keeps the last two:

CODE
GET /v1/geo?include=rules,states&lang=th   per visitor, never cached
GET /v1/countries?lang=th                  cached
GET /v1/locales/th                         cached

GET /v1/geo

Where the visitor is, from Cloudflare's own request properties: no geo-IP service and no added latency. The response is a GeoResponse.

ip is the cf-connecting-ip header and nothing else: x-forwarded-for is whatever the client wrote there. The address identifies a person, which is why this route is never cached. Cloudflare's T1 (a Tor exit) is reported as no country.

?include=rules adds rules, a CountryResponse for the country the form should open on, and ?include=rules,states puts that country's state options in it. That country is ?country= when given; else the detected one, when ?countries= is absent or lists it; else the first entry of ?countries=; else the US. The top-level country still says where the visitor is, which is what a page needs to offer "ship somewhere else?".

GET /v1/countries/:country

One country's rules: which fields a form asks for, in what rows, and everything about each of them. The response is a CountryResponse; every field is a Field. Add ?include=states for the options of the state list.

A code with no rules of its own, or no country behind it, returns 200 with the default layout under that code and curated: false, never a 404: a visitor from such a place still has to be able to check out.

Every country's rules can be read in these docs, under Countries.

The layout

address.layout is rows of field names, in the order the country writes an address. A field not in the layout is not asked for and is not in fields. The layout is the whole address, the name and the phone number included, because a billing address is one.

The email is the exception: it is in fields for every country and in no row, because it belongs to the person rather than to an address, and a form asks for it once however many addresses it shows. Where it goes is the client's choice.

address.fixed holds values every address in the country shares, which a client sends without asking. It is empty in the US; Vatican City's is { "city": "Vatican City", "postcode": "00120" }. A fixed field is never in the layout.

Writing an address

address.format is how the country writes an address for reading: on an order page, a receipt, a label or a debugging tool. It is not the form, and the two often differ: the US form asks for city, state and ZIP Code in three boxes, and the written address is Mountain View, California 94043 on one line. See AddressSource.

Each line is a list of parts, a field or text. A field with no value is left out, and so is the text beside it, and a line left with nothing is dropped: {city}, {state} {postcode} with no state writes Mountain View 94043. format_latin is how the country writes an address in Latin script, where that differs. The US has none, because its format is Latin already; a Japanese address in Japanese starts from the postcode and ends with the name, and in English starts with the name, given name first. Use it when the address is in Latin script.

state is written by its name. An order stores the state's code, which in the US, Canada and Australia is an abbreviation, so add ?include=states and look the code up in states; write the stored value as it is only where no option has it. upper lists the fields a country's post writes in capitals on an envelope; a page on a screen can leave them as they are.

address.example is a real address in the country, a public building's, in its own script, and example_latin the same address in Latin script: for previewing a form or a written address. Every curated country has one. Each country's page under Countries shows its format and its example written out.

Labels and messages

label_id says where a field's texts are in /v1/locales/:lang: fields.postcode.zip_code holds a US postcode's label, label_optional and errors. A page that overrides a text gives that key plus the text's own, fields.postcode.zip_code.errors.invalid.

label has no note about being optional and label_optional does. A client shows the one required calls for, or the plain label where the page itself requires a field the rules do not. errors holds whole sentences, with the example and the length limit filled in; a field carries only the errors it can raise (see FieldError). A postcode's not_in_state is not among them: it names the state the shopper chose, so only POST /v1/validate answers it, with the state filled in.

Postcodes

A postcode's pattern is matched against the postcode upper-cased with spaces and hyphens removed, so 94103-1234 and 941031234 are the same ZIP+4 code. masks say how it is written. A country on the default layout has no postcode format: its postcode is accepted as typed. See PostcodeFormat.

Whether a postcode is the one the chosen state uses is not in the rules: send both to POST /v1/validate. Each country's page under Countries lists how its states' postcodes start.

Phone numbers

The value the phone field submits is E.164, +14155552671: that is what the tel token means. The rule shows the number as it is typed (masks) and refuses what is clearly not a phone number (pattern); it does not build E.164. That comes from POST /v1/validate, which reads the number with libphonenumber itself, including a number dialled abroad or with another country's code. See PhoneRule.

The rule is loose on purpose: the order API validates the number, so pattern only refuses what is clearly not a phone number. A country on the default layout has a calling code, its national prefix where it has one, and an example, and its number is not checked. Seven uninhabited territories have no phone data at all.

Currency and region

currency is the ISO 4217 currency in circulation in the country. Read it as a fact about where the visitor is, not as an instruction about what to charge: pricing is the storefront's decision. Where more than one currency is legal tender, the longest established is named (the rand in Namibia, the Indian rupee in Bhutan). Antarctica has none, and no currency key.

region is the country's place in the UN M49 scheme, as CLDR nests it: one of five regions, and one of its subregions.

RegionDescription
002Africa
009Oceania
019Americas
142Asia
150Europe

The codes are the same in every language; name is in lang. The subregions are CLDR's, which split Africa and the Americas finer than some tables do: the US is 021, Northern America, inside 019, the Americas.

GET /v1/countries

The country list, for a country dropdown: a CountrySummary per country, named and sorted in ?lang=. ?countries= restricts it. What a country asks for is in its rules, not in this list.

GET /v1/countries/:country/states

A country's state options, as StateOptions, or [] for a country whose subdivisions are not listed:

CODE
[{ "code": "AL", "name": "Alabama" }]

code is the value a form submits, and what it holds varies by country; see State values. name is in ?lang= where the ISO 3166-2 catalog translates it, and in English otherwise, and the list is sorted in that language's order. The same options are in states on the two routes that take ?include=states.

GET /v1/locales/:lang

One language's texts, as i18next JSON, so an i18next client can load it as a resource:

CODE
{ "fields": { "postcode": { "zip_code": { "label": "ZIP Code" } } } }

en-US is served as en. A language with no file gets English, as every other route does, and content-language says which language the body is in; not a 404, which a browser would log to the console of every visitor on a page in that language.

A field's texts are also in its rules, resolved for the country. This route is the whole file: for a page's own headings (checkout.*), for its hosted card fields and the sentence for each declined payment (payment.*, see Payment errors), for what a coupon field says back (coupon.*), and for a client that wants every variant. Every language's texts can be read under Languages.

GET /v1/flags/:code.svg

The country's flag, 4:3, as SVG: /v1/flags/gb.svg, with the code in lower case. Every country in the country list has one; any other code is a 404. The flags are flag-icons, served as static files and cached for a week.

POST /v1/validate

Checks what a shopper typed into a form for an address in country, as a ValidateRequest, and answers each field it was sent: whether the value is one the country takes, and the value to submit for it. A page sends one field as it is typed and every field on submit, in one request.

FieldDescription
phone_numberA PhoneNumberResult
postcodeA PostcodeResult
any other{ "valid": null }, a FieldResult

Both answers extend FieldResult, which every field's answer has: valid, and value or error.

CODE
{ "country": "US", "fields": { "phone_number": "(415) 555-2671" } }
CODE
{
  "lang": "en",
  "fields": {
    "phone_number": {
      "valid": true,
      "value": "+14155552671",
      "country": "US",
      "type": "fixed_line_or_mobile",
      "national": "(415) 555-2671",
      "international": "+1 415 555 2671"
    }
  }
}

A phone number is read the way libphonenumber reads it, typed in the address country: a national number, one with its own + (+1 415 555 2671), or one dialled abroad with the country's own prefix (011 44 7400 123456 in the US). A number starting 00 that libphonenumber finds no number in is read again with the 00 as a +, because that is how a form is filled in anywhere: 0044 7400 123456 typed on a US address is +447400123456, though 00 is no US prefix. Digits typed in Thai or full width are read as digits.

A valid number also says, from libphonenumber (see PhoneNumberResult):

KeyDescription
valueThe number in E.164, to submit
countryThe number's own country
typemobile, fixed_line, toll_free, …
nationalHow its country writes it at home
internationalHow it is written from abroad

country need not be the address country: 011 44 7400 123456 typed on a US address is GB. type is fixed_line_or_mobile where the country's numbers do not say which, as in the US, and toll_free for (800) 555-0199. national and international are for showing the number once it has been read, in any country, including the ones whose rules have no mask; an extension typed after the number stays in them and is not in value.

A postcode is checked against the country's format.pattern and written with its format.masks, so 941031234 is 94103-1234. Send the form's state beside it and the postcode is checked against that state too: 10001 is a US ZIP code, and not California's.

CODE
{ "country": "US", "fields": { "postcode": "10001", "state": "CA" } }
CODE
{
  "lang": "en",
  "fields": {
    "postcode": {
      "valid": false,
      "error": { "code": "not_in_state", "message": "Enter a valid ZIP Code for California" },
      "state": "NY"
    },
    "state": { "valid": null }
  }
}

Which postcodes a state takes comes from libaddressinput, as how they start: California's are 9[0-5]|96[01], so 90 to 95, 960 and 961. The countries it has them for are listed in How countries differ, and each country's page under Countries lists its states' prefixes.

A postcode also says (see PostcodeResult):

KeyDescription
valueAs its country writes it, when valid
stateThe state it is in, as a form submits it

state comes only where one state's postcodes start that way, and comes with not_in_state too, so a form can offer that state instead.

Nothing is guessed. A state with no prefix is checked by format only, and a country on the default layout, or a request with no country, is answered { "valid": null }. A few real addresses sit across a state line from their postcode (Texhoma, Texas has an Oklahoma ZIP code), so a form that refuses not_in_state refuses those too; one that would rather not shows the message and lets the order through.

ResponseDescription
200Every field answered, passing or not
400A body that cannot be read
429Too many requests from one address

A field that does not pass has "valid": false and an error: a code to branch on and a message to show (see ValidationError).

CodeDescription
blankNothing to read
invalidNot a number or postcode the country uses
not_in_stateA postcode the state sent does not use

The message is the same sentence the field's rules carry for that code, with the example filled in, and for not_in_state the name of the state sent: Enter a valid ZIP Code for California. A phone number typed with another country's code shows that country's example, written with its +: +6683873196 on a US address is Enter a valid phone number, like +66 81 234 5678, not the US example. It is in ?lang=, else the first language of Accept-Language the API serves, else English, as on every route; the answer's lang and its content-language header say which. A browser sends its own Accept-Language, so a page whose language is not the browser's sends ?lang=. The code is the key the message is under in GET /v1/locales/:lang, for a page that writes its own. A value that does not pass has no value, so a client sends it as typed and the order API reads it.

The values are what a shopper typed, so the answer is private, no-store and the Worker logs none of them. Call it with fetch(): navigator.sendBeacon sends a ping, which EasyPrivacy blocks for every third-party host.

GET /v1/version and GET /v1/health

/v1/version answers { "data": "je0e35" }, the data version: the same string as version in every country's rules. /v1/health answers { "ok": true } and is never cached.