Installation Tokens
Mint short-lived bi_oat_ tokens for scoped API access.
The access token from the authorization flow cannot read resources directly. To call partner API endpoints, exchange it for a 1-hour installation token:
Mint a token
curl -X POST \
https://api.better-i18n.com/api/oauth-client/installations/<grant_id>/tokens \
-H "Authorization: Bearer <access_token>"Response
{
"installation_token": "bi_oat_d7ab12...",
"token_type": "Bearer",
"expires_at": "2026-04-30T12:00:00.000Z",
"expires_in": 3600,
"organization_id": "org_...",
"project_ids": [],
"scopes": ["org:read", "keys:read", "keys:write", "translations:write"]
}project_ids: [] means all projects in this organization. A non-empty array restricts access to those specific projects.
Caching strategy
Mint a new token only when the cached one is within ~60 seconds of expiring:
let cache: { token: string; expiresAt: number } | null = null;
async function getInstallationToken(
grantId: string,
accessToken: string,
): Promise<string> {
if (cache && cache.expiresAt - Date.now() > 60_000) {
return cache.token;
}
const res = await fetch(
`https://api.better-i18n.com/api/oauth-client/installations/${grantId}/tokens`,
{
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
},
);
const data = await res.json();
cache = {
token: data.installation_token,
expiresAt: new Date(data.expires_at).getTime(),
};
return cache.token;
}Do not mint per request. Every mint is logged in the audit trail and rate-limited. In practice you need 1-2 mints per hour.
What's inside the token
The installation token is a Better i18n API key (apikey table row) with embedded permissions:
{
"type": "installation",
"grantId": "grt_...",
"organizationId": "org_...",
"projectIds": [],
"scopes": ["keys:read", "keys:write"]
}The middleware reads these permissions on every request — no additional lookup needed.
Next step
Use the installation token to call resource APIs.