Requests and Responses
Endpoint
GET ~/loc-2.v1?<lson>
HEAD ~/loc-2.v1?<lson>
POST ~/loc-2.v1
~ is your Agility instance root, for example
https://www.example.com/VersionOne. Responses are application/json.
~/loc-2.img is registered to the same handler and behaves identically; it
exists only so that callers can vary the URL for cache-busting purposes.
The Query String Is Not a Normal Query String
The entire raw query string — everything after the ?, with no name=value
structure — is URL-decoded and then parsed as LSON, Agility's lenient JSON
dialect. What you send determines the shape of what you get.
Array in, Object Out
This is the form you almost always want.
curl "https://www.example.com/VersionOne/loc-2.v1?[Scope,Timebox,Story]" \
-H "Accept-Language: en-US"
{
"Scope": "Project",
"Timebox": "Sprint",
"Story": "Story"
}
Keys are the tags exactly as you sent them, in request order. Duplicate tags collapse into a single entry.
Scalar in, String Out
curl "https://www.example.com/VersionOne/loc-2.v1?Scope"
"Project"
Note the response is a bare JSON string, not an object. Prefer the array form even for one tag so your parsing stays uniform.
Edge Cases
| Query string | Response | Notes |
|---|---|---|
?[Scope,Scope] | one entry | Duplicates collapse |
?[] | {} | Empty array |
?[Scope,,Timebox] | "": "" | An empty element resolves to an empty string, unresolved |
? (nothing) | "" | Empty query string parses as the empty string |
?null | null | The literal token null parses as JSON null |
Quoting Rules You Will Eventually Hit
LSON accepts 'single' or "double" quotes around a string, and treats an
unquoted run of characters as a bare token that ends at whitespace, , or ].
Localization tags separate their terms with a tic ('), which interacts with
that:
- A tic inside a bare token is fine.
[AttributeDefinition'Name'Story]parses as one tag, because quoting is only considered at the start of a value. - A tag that starts with a tic would be read as an opening quote. Quote it.
- A tag containing a space, a comma or a
]must be quoted, or it will be split.
When in doubt, wrap every tag in double quotes and URL-encode the result:
curl -G "https://www.example.com/VersionOne/loc-2.v1" \
--data-urlencode '=["AttributeDefinition'"'"'Name'"'"'Story","Plural'"'"'Story"]'
POST for Long Tag Lists
A GET query string is subject to your web server's URL length limit. For large batches, POST the same LSON payload as the request body. The body is URL-decoded and then parsed identically.
curl -X POST "https://www.example.com/VersionOne/loc-2.v1" \
--data-urlencode '["Scope","Timebox","Story","Plural'"'"'Story"]'
POST responses are marked Cache-Control: no-cache.
Authentication
loc-2.v1 does not require authentication. The handler overrides the
authentication step to always succeed, so no credentials are needed and none
are validated.
Sending an Authorization header anyway is harmless — it is simply ignored.
Localization output is not user-specific; it depends only on the requested
culture and the instance's configured terminology.
Choosing a Culture
Set Accept-Language. Agility reads only the first entry of the header and
converts it to a specific culture:
Accept-Language: fr-CA -> fr-CA
Accept-Language: fr -> fr-FR (neutral cultures are expanded to their default specific culture)
Accept-Language: xx-YY -> invariant culture (unparseable input is swallowed)
(header absent) -> invariant culture
Because a neutral culture is expanded before it reaches the localizer, you cannot address a neutral culture's templates directly over HTTP — you always land on a specific culture, which then falls back. What that fallback does is covered in Culture fallback.
Caching
GET and HEAD responses are cacheable:
| Header | Value |
|---|---|
Cache-Control | public, max-age=3600 |
Vary | includes Accept-Language |
ETag | derived from the newest write time in the strings folder |
Send If-None-Match with a previously returned ETag and you get 304 Not Modified with an empty body. There is an important caveat about ETag staleness
after a template change — see
Hot reload and caching.
Errors
| Status | Cause |
|---|---|
405 | Any method other than GET, HEAD, POST or OPTIONS. Response carries Allow: GET,HEAD,POST,OPTIONS. |
500 | The payload parsed, but an element was not a string — for example ?[1,2] or ?[{a:b}]. The message is Unsupported Type: <TypeName>. |
500 | Any unexpected server-side failure. |
OPTIONS returns 200 with the Allow header and no body.
Error bodies are text/plain, not JSON — the content type is reset before the
error is written. Do not assume a JSON body on a non-2xx response from this
endpoint.
There is no 400 for an unrecognised tag, because there is no such thing: an
unmatched tag resolves to itself. See
Tag resolution.
Legacy loc.v1
~/loc.v1 predates loc-2.v1 and is still registered.
loc.v1 | loc-2.v1 | |
|---|---|---|
| Methods | GET, HEAD | GET, HEAD, POST |
| Payload | one raw tag as the whole query string | LSON scalar or array |
| Content type | text/plain | application/json |
| Empty query string | 400 Missing TAG Parameter | "" |
curl "https://www.example.com/VersionOne/loc.v1?AttributeDefinition'Name'Story"
Title
Both endpoints resolve tags through exactly the same localizer, so every
semantic page in this section applies equally to both. Use loc-2.v1 for new
work: one round trip for a whole screen's worth of labels beats one round trip
per label.
Corrections to Older Documentation
Older descriptions of this API are still in circulation. Where what you have read disagrees with this section, these are the corrections:
| You may have read | The reality |
|---|---|
The endpoint is loc.v1 | That is the legacy endpoint. loc-2.v1 exists and is the bulk form. |
Responses are text/plain | True of loc.v1. loc-2.v1 returns application/json. |
Requests need an Authorization header | No authentication is required or checked. |
The override set is called Custom | Custom is a sample application's set name. Agility's in-field override file is Company. See Template sets. |