Company context, in your application.
Read legal-entity records through the FTM API. Start with one signed request, then connect the data to your own server workflows.
Endpoint catalog →Your first request
- 01
Create a key
Sign in and open Account → API keys. Choose a name, select legal-entities:read, and set an optional expiration. If API keys is not available on your account, contact FTM support.
- 02
Save the secret
Copy the secret when it appears and store it securely on your server. It cannot be retrieved later. Use a separate key for each integration.
- 03
Call a legal-entity endpoint
Set the four environment variables below, then run either example. Use a real 11-digit registration number and the API origin for your environment.
export FTM_API_BASE='https://api.ftm.lv'
export FTM_REGCODE='YOUR_11_DIGIT_REGISTRATION_NUMBER'
export FTM_API_KEY='ak_YOUR_PUBLIC_KEY'
# Supply FTM_API_SECRET through your server's secret manager.
# For an interactive shell (bash), avoid putting it in shell history:
read -rsp 'API Secret: ' FTM_API_SECRET; export FTM_API_SECRET; echo
python3 ftm_api.py
# or: node ftm-api.mjsSet FTM_API_BASE to https://api.ftm.lv. Create an API key in Account → API keys in this environment. Endpoints start with /service/api/v1.
For server applications
HMAC secrets belong on your server. Do not put them in browser JavaScript, public SPA bundles or mobile apps. The JavaScript example runs in Node.js.
Signing a request
Each request carries a fresh timestamp and random nonce. Sign the six fields below with HMAC-SHA256 using the literal API Secret string as UTF-8 bytes. Send the result as 64 lowercase hexadecimal characters. Never send the secret itself.
HTTP_METHOD
EXACT_ESCAPED_PATH
CANONICAL_QUERY
TIMESTAMP
NONCE
SHA256_RAW_BODYJoin the fields with exactly five LF characters. There is no trailing newline. Hash the exact body bytes, including whitespace; an empty body still has a SHA-256 hash.
Path: sign the exact escaped path sent on the wire. Do not decode, clean or re-encode it. Percent-escape case, trailing slashes and repeated slashes are significant. /foo%2Fbar, /foo%2fbar, /foo_bar, /foo/bar/ and /foo//bar are all different.
Query: decode percent escapes and + as space, sort decoded names by UTF-8 bytes, and preserve the order of duplicate values. Encode names and values using form encoding: spaces become +, a literal + becomes %2B, and escapes use uppercase hex. Unreserved characters A–Z a–z 0–9 - . _ ~ stay literal. Bare keys become key=. Reject malformed escapes and unescaped semicolons. An empty query is an empty field.
| Input | Canonical query |
|---|---|
| b=2&a=1 | a=1&b=2 |
| a=2&a=1&a=2 | a=2&a=1&a=2 |
| q=a%20b | q=a+b |
| q=a+b | q=a+b |
| q=%2B | q=%2B |
| flag&empty= | empty=&flag= |
| "" | "" |
HMAC test vectors
Offline fixtures with a fake key and secret. Use literal UTF-8 secret and body bytes, without a BOM or trailing newline. The canonical string has five LF separators and no final LF; JSON escapes below show exact bytes. The API key selects the credential but is not a canonical-string field. The POST vector tests encoding only; it is not a supported operation. Fixed timestamps and nonces are for unit tests, not live requests.
Download test vectors (JSON){
"name": "empty-get",
"key": "ak_documentation_test_only",
"secret": "ftm_fake_secret_DO_NOT_USE",
"method": "GET",
"path": "/service/api/v1/legal-entities/40000000000",
"query": "",
"body": "",
"timestamp": 1789336800,
"nonce": "AAECAwQFBgcICQoLDA0ODw",
"canonical_query": "",
"body_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"canonical": "GET\n/service/api/v1/legal-entities/40000000000\n\n1789336800\nAAECAwQFBgcICQoLDA0ODw\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signature": "1a0314746224b5356b87590de0a404b5382ef36074020f4890ebfc878483b66f"
}{
"name": "utf8-body-and-query",
"key": "ak_documentation_test_only",
"secret": "ftm_fake_secret_DO_NOT_USE",
"method": "POST",
"path": "/service/api/v1/%C4%81//x%2Fy/",
"query": "z=last&a=2&a=1&space=a%20b&plus=%2B&utf=%C4%81",
"body": "{\"message\":\"Sveiki, Rīga\"}",
"timestamp": 1789336800,
"nonce": "AAECAwQFBgcICQoLDA0ODw",
"canonical_query": "a=2&a=1&plus=%2B&space=a+b&utf=%C4%81&z=last",
"body_sha256": "5d0358713e4db6811b419f86478083965ad4bfdcd1919dfe1f5c2f19c58907d6",
"canonical": "POST\n/service/api/v1/%C4%81//x%2Fy/\na=2&a=1&plus=%2B&space=a+b&utf=%C4%81&z=last\n1789336800\nAAECAwQFBgcICQoLDA0ODw\n5d0358713e4db6811b419f86478083965ad4bfdcd1919dfe1f5c2f19c58907d6",
"signature": "6d57bebafb27ec31447339a28615802400e3a8f3b3c1151c200c8a0fc3e5b01c"
}Required request headers
- X-API-Key
- Public key, starting with ak_.
- X-API-Timestamp
- Unix timestamp in whole seconds. Keep your server clock synchronized.
- X-API-Nonce
- A new nonce per request: at least 16 secure random bytes, base64url without padding. Accepted length: 22–128 characters.
- X-API-Signature
- 64 lowercase hexadecimal characters: HMAC-SHA256(secret, canonical request).
Python & JavaScript examples
Complete, dependency-free clients. They preserve the request target and body, verify HTTPS certificates, and do not follow redirects. Re-sign every retry with a fresh nonce.
"""Python 3.10+, standard library only. Keep API secrets on your server."""
import base64
import hashlib
import hmac
import http.client
import os
import re
import secrets
import sys
import time
from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit
def canonical_query(raw):
if ';' in raw or re.search(r'%(?![0-9A-Fa-f]{2})', raw):
raise ValueError('Invalid query encoding')
pairs = parse_qsl(raw, keep_blank_values=True, encoding='utf-8', errors='strict')
# Stable sort by UTF-8 key; preserve duplicate-value ordering.
pairs.sort(key=lambda pair: pair[0].encode('utf-8'))
return urlencode(pairs, quote_via=quote_plus, safe='~')
def canonical_request(method, path, query, timestamp, nonce, body=b''):
if not re.fullmatch(r"[!#$%&'*+.^_`|~0-9A-Za-z-]+", method):
raise ValueError('Invalid HTTP method')
if not re.fullmatch(r"/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*", path):
raise ValueError('Use the exact escaped path')
if not re.fullmatch(r'[A-Za-z0-9_-]{22,128}', nonce):
raise ValueError('Invalid nonce')
if type(timestamp) is not int or timestamp < 0:
raise ValueError('Invalid timestamp')
return '\n'.join([method.upper(), path, canonical_query(query), str(timestamp), nonce,
hashlib.sha256(body).hexdigest()])
def sign(secret, method, path, query, timestamp, nonce, body=b''):
canonical = canonical_request(method, path, query, timestamp, nonce, body)
# Do not base64-decode the issued secret. Use its literal UTF-8 bytes.
return hmac.new(secret.encode('utf-8'), canonical.encode('utf-8'), hashlib.sha256).hexdigest()
def main():
base = urlsplit(os.environ['FTM_API_BASE'])
key, secret, regcode = (os.environ[k] for k in ('FTM_API_KEY', 'FTM_API_SECRET', 'FTM_REGCODE'))
if not re.fullmatch(r'[0-9]{11}', regcode):
raise ValueError('FTM_REGCODE must contain 11 digits')
if not base.hostname or base.username or base.password or base.path not in ('', '/') or base.query or base.fragment:
raise ValueError('FTM_API_BASE must be an origin without credentials, path, query or fragment')
if base.scheme != 'https':
raise ValueError('HTTPS required')
method, path, query, body = 'GET', '/service/api/v1/legal-entities/' + regcode, '', b''
timestamp = int(time.time())
nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).rstrip(b'=').decode('ascii')
headers = {'X-API-Key': key, 'X-API-Timestamp': str(timestamp), 'X-API-Nonce': nonce,
'X-API-Signature': sign(secret, method, path, query, timestamp, nonce, body),
'Accept': 'application/json'}
connection = http.client.HTTPSConnection(base.hostname, base.port, timeout=30)
try:
# http.client sends this exact escaped target and does not follow redirects.
connection.request(method, path + ('?' + query if query else ''), body=body, headers=headers)
response = connection.getresponse()
while chunk := response.read(65536):
sys.stdout.buffer.write(chunk)
sys.stdout.buffer.write(b'\n')
return 0 if response.status == 200 else 1
finally:
connection.close()
if __name__ == '__main__':
try:
sys.exit(main())
except (KeyError, ValueError, OSError, http.client.HTTPException):
sys.exit('Request failed. Check configuration, connectivity and API access.')
Access & operation
Credential scopes
Scopes control which endpoints a key can call and which data it can receive. Request only the scopes your integration needs. Restricted personal details are available only when both the key and its account have the appropriate access.
Limits & errors
The default signature window is ±300 seconds; an operator can make it shorter. A nonce cannot be reused with the same key during the accepted window, including future clock skew. Rate limits currently allow 100 authenticated requests per minute per key per backend instance, plus the existing IP limit. Respect 429 and Retry-After when present; use backoff and a fresh signature.
- identity:read
- Check which API key is authenticated.
- legal-entities:read
- Read the full legal-entity record and its available relationships. Personal identifiers are omitted.
- legal-entities:personal-details
- Include restricted personal details. Requires legal-entities:read and an account approved for this data.
401: missing, invalid, expired, revoked or replayed authentication, insecure transport or unsupported body encoding. 403: required scope or permission is missing. 400: invalid input. 404: record or route not found. 413: body too large. 429: rate limit. 500: server failure. Error bodies use code and message_key; do not depend on English text.
{
"code": "invalid_api_authentication",
"message_key": "error.unknown"
}Stable service API error codes
These codes apply to the signed service endpoints. Credential-management screens use separate session endpoints. Unknown routes, unsupported methods, oversized bodies or upstream failures may return non-JSON responses: always check HTTP status and Content-Type, and handle unknown codes safely.
- 400 · invalid_registration_code
error.registry.invalid_registration_codeUse exactly 11 ASCII digits for regcode.
- 401 · invalid_api_authentication
error.unknownCheck HTTPS, all four headers, clock, nonce, signature, active key and account eligibility. Authentication failures intentionally share one code; do not retry unchanged.
- 403 · forbidden
forbidden.messageThe key lacks the required scope or the owner no longer has the required entitlement. Correct access before retrying.
- 404 · not_found
error.registry.record_not_foundNo legal entity exists for this registration number. Check the number; do not repeatedly retry it.
- 413 · body_too_large
error.invalid_inputReduce the request body; the limit is 4 MiB. An earlier server/proxy limit may instead return a non-JSON 413.
- 429 · rate_limited
error.rate_limitedWait for Retry-After when supplied, otherwise use exponential backoff. Sign each retry with a fresh nonce and timestamp.
- 500 · db_error
error.database.query_failedTemporary server/database failure. Retry with bounded backoff and a newly signed request; report persistent failures.
Up to 100 active, unexpired keys per owner. Revocation takes effect on the next authentication across all instances. Rotate by creating a replacement, updating your server, then revoking the old key. Last-used timestamps may be coalesced to about one minute. Secrets are encrypted at rest with AES-256-GCM.
API requests require HTTPS. Use the matching staging or production API origin and keep the API secret on your server.