Authentication
Every request is authenticated with the credentials BoxLatvia issued to you. Use HTTP Basic for the simplest integration, or OAuth 2.0 if your tooling expects it. Both give the same access.
#HTTP Basic
Send the client ID as the user name and the client secret as the password on every request. Almost every HTTP library does this for you.
curl "$API/v1/me" \
-u "$CLIENT_ID:$CLIENT_SECRET"const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/me`, {
headers: { Authorization: auth },
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.get(
f"{API}/v1/me",
auth=(CLIENT_ID, CLIENT_SECRET),
)
print(r.status_code, r.json())Always use HTTPS. The API never accepts credentials over plain HTTP.
#OAuth 2.0 client credentials
Exchange your credentials for a short-lived bearer token and send that instead. Tokens last 15 minutes: when the API answers 401, request a new one. Cache the token and renew it a minute early, rather than requesting one per call.
#Get an access token
/v1/oauth/tokenno scope neededOAuth 2.0 client credentials: exchange your client id and secret for a bearer token valid for 15 minutes.
Send the credentials as HTTP Basic (as in the example) or as client_id and client_secret in the form body. Errors follow the OAuth standard (invalid_client, invalid_scope, …) rather than the usual problem format.
Request body (application/x-www-form-urlencoded)
| Name | Type | Description |
|---|---|---|
grant_type required | string | Always client_credentials. |
scope | string | Space-separated scopes you want the token to carry. Default: everything your credentials allow. You cannot ask for more than that. |
Examples
curl -X POST "$API/v1/oauth/token" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d "scope=boxes:read boxes:write"const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/oauth/token`, {
method: "POST",
headers: { Authorization: auth },
body: new URLSearchParams({
"grant_type": "client_credentials",
"scope": "boxes:read boxes:write"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.post(
f"{API}/v1/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"grant_type": "client_credentials",
"scope": "boxes:read boxes:write",
},
)
print(r.status_code, r.json()){
"access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FwaS5ib3hsYXR2aWEubHYi…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "boxes:read boxes:write"
}curl -X POST "$API/v1/oauth/token" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentialsconst API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/oauth/token`, {
method: "POST",
headers: { Authorization: auth },
body: new URLSearchParams({
"grant_type": "client_credentials"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.post(
f"{API}/v1/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"grant_type": "client_credentials",
},
)
print(r.status_code, r.json()){
"error": "invalid_client",
"error_description": "Invalid client credentials."
}Errors
| Status | When |
|---|---|
| 400 | invalid_request, unsupported_grant_type or invalid_scope. |
| 401 | invalid_client: the id or secret is wrong, or the access was revoked or has expired. |
| 429 | Too many failed attempts from your address. |
#Use the token
curl "$API/v1/me" \
-H "Authorization: Bearer $TOKEN"The token only carries the scopes you asked for. Revoking your credentials or narrowing their scopes takes effect on the very next request, even for tokens that were already issued. The signing keys are published at /v1/.well-known/jwks.json.
#Scopes
Scopes decide what a set of credentials may do. Ask only for what the integration needs.
| Scope | Allows |
|---|---|
reference:read | Read the reference lists (amenities, districts, price units, sizes). |
supplier:read | Read your supplier profile and GET /v1/me. |
supplier:write | Change your profile's contact and address fields. |
locations:read | List and read locations and their pictures. |
locations:write | Create, change and delete locations. |
boxes:read | List and read boxes and their pictures. |
boxes:write | Create, change and delete boxes; submit and withdraw them. |
images:write | Upload and delete pictures of boxes and locations. |
A call without the right scope is answered with 403 and the missing scope in required_scope.
curl -X POST "$API/v1/boxes" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "Box",
"location_id": "9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3"
}'const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/boxes`, {
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Box",
"location_id": "9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.post(
f"{API}/v1/boxes",
auth=(CLIENT_ID, CLIENT_SECRET),
json={
"name": "Box",
"location_id": "9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3",
},
)
print(r.status_code, r.json()){
"type": "https://api.boxlatvia.lv/problems/insufficient-scope",
"title": "Forbidden",
"status": 403,
"detail": "This operation needs the scope boxes:write.",
"request_id": "d504c7e6-8458-456a-884d-0b4ba02be2d3",
"required_scope": "boxes:write"
}#Restrictions on credentials
- IP allow-list. If BoxLatvia sets one for you, calls from any other address are refused with
403. - Expiry. Credentials can have an end date, after which they stop working.
- Revocation and rotation. BoxLatvia can revoke credentials or issue a new secret at any time; the old secret stops working at once. Tell us immediately if a secret may have been exposed.
#Failed logins
After 10 failed authentications within 5 minutes from one IP address, further calls from that address are answered with 429 and a Retry-After header (in seconds) until the window passes. Do not retry a 401 in a loop.
curl "$API/v1/me" \
-u "$CLIENT_ID:$CLIENT_SECRET"const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/me`, {
headers: { Authorization: auth },
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.get(
f"{API}/v1/me",
auth=(CLIENT_ID, CLIENT_SECRET),
)
print(r.status_code, r.json()){
"type": "https://api.boxlatvia.lv/problems/too-many-requests",
"title": "Too many requests",
"status": 429,
"detail": "Too many failed authentication attempts. Try again later.",
"request_id": "88b1f62d-3509-4d1f-b542-ff67a35787ae"
}#Keeping secrets safe
- Keep the secret in a secrets manager or an environment variable, never in source code, in front-end code or in logs.
- Use separate credentials per system and per environment.
- Call the API from your servers only, never from a browser or a mobile app.