---
id: requests-and-responses
title: 'Requests and Responses'
description: 'Methods, query-string syntax, response shapes, authentication, errors and caching for the loc-2.v1 localization endpoint.'
sidebar_label: 'Requests and Responses'
sidebar_position: 2
tags: ['api', 'localization', 'reference']
---

## Endpoint

```text
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.

```bash title="Bulk request"
curl "https://www.example.com/VersionOne/loc-2.v1?[Scope,Timebox,Story]" \
  -H "Accept-Language: en-US"
```

```json
{
  "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

```bash title="Single-tag request"
curl "https://www.example.com/VersionOne/loc-2.v1?Scope"
```

```json
"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:

```bash title="Always-safe form"
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.

```bash title="POST form"
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:

```text
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](/apis/loc-2-v1/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](/apis/loc-2-v1/hot-reload#the-etag-caveat).

## 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](/apis/loc-2-v1/tag-resolution#unmatched-tags-resolve-to-themselves).


## 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` | `""` |

```bash title="Legacy single-tag request"
curl "https://www.example.com/VersionOne/loc.v1?AttributeDefinition'Name'Story"
```

```text
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](/apis/loc-2-v1/template-sets#the-set-stack-agility-actually-uses). |

## Related Topics

- [Tag resolution](/apis/loc-2-v1/tag-resolution)
- [Culture fallback](/apis/loc-2-v1/culture-fallback)
- [HTTP status codes](/reference/status-codes)
- [Error handling](/guides/error-handling)
