Authentication
API version 2026-10-affable-towhee
Every call to the Lingara API carries an access token. A server gets one by exchanging an OAuth client's ID and secret at the token endpoint: OAuth 2.0's client credentials grant, for a server acting as itself. Create clients on the Integrations page of the Lingara web app, at app.getlingara.com/admin.
A session or a client
The Lingara apps sign you in with a session, which holds every scope and spends your plan's allowance. A client is narrower: you choose the scopes it is allowed when you create it, and each access token it gets carries only the scopes it asks for.
Three values
The client ID begins lgr_cid_ and names the client at the token endpoint. It is not a secret.
The client secret begins lgr_cs_ and is sent only to the token endpoint. It is shown once, when you create it: copy it then, because Lingara stores only a hash of it.
The access token begins lgr_at_ and lasts one hour. It goes in the Authorization: Bearer header of calls under /v1/, and nowhere else. It is the only value that belongs in that header: a client secret sent there is refused with 401.
Get an access token
POST a form to the token endpoint with grant_type=client_credentials. Send the client ID and secret either with HTTP Basic authentication or as the client_id and client_secret form fields, never both. scope is a space-separated list of scopes the client is allowed; leave it out to get every scope the client is allowed. The command below asks for usage:read alone, the scope the GET /v1/usage example further down needs.
curl -X POST "https://api.getlingara.com/oauth/token" \
-u "$LINGARA_CLIENT_ID:$LINGARA_CLIENT_SECRET" \
-d "grant_type=client_credentials" \
--data-urlencode "scope=usage:read"The response carries access_token, token_type (Bearer), expires_in (3600, in seconds) and scope, the scopes the token actually holds. A failed exchange answers in the OAuth error shape, {error, error_description}, not the {code, error} envelope of /v1/. A wrong or revoked secret, or a deleted client, is refused with 401 and invalid_client. A scope the client may not request refuses the whole exchange with 400 and invalid_scope; it is never narrowed silently.
Act for another Lingara user
An app acting for another Lingara user uses the authorization code grant. Send the user's browser to the authorization URL with response_type=code, client_id, a redirect_uri that exactly matches one you registered, scope, state and an S256 code_challenge. The user sees Lingara's consent page and returns to your redirect_uri with code, state and iss. Check that state is the one you sent and that iss is https://api.getlingara.com before you use the code.
PKCE is required
Every client uses PKCE, with the S256 method only. Make a random code_verifier, send its SHA-256 hash, base64url-encoded, as code_challenge with code_challenge_method=S256, and keep the verifier for the exchange. A request with no method, or with plain, is refused with invalid_request.
Exchange the code
Within 60 seconds, POST to the token endpoint with grant_type=authorization_code, code, the same redirect_uri and code_verifier, authenticating as the client; a public client sends client_id alone. The response adds a refresh_token, which begins lgr_rt_. The code begins lgr_ac_ and works once: a second use is refused with invalid_grant and ends the tokens the first exchange issued.
Refresh
When the access token expires, POST to the token endpoint with grant_type=refresh_token and the refresh_token, authenticating as the client again. Every refresh returns a new refresh token: keep only the newest. Serialise your refreshes: an old refresh token used more than 60 seconds after it was replaced is treated as stolen, and ends that install's tokens with invalid_grant. A refresh token unused for 30 days expires. The token endpoint limits requests per address, so refresh only when a token runs out.
Native apps are public clients
A desktop, mobile or command-line app cannot keep a secret, so it is a public client: it has no secret, sends client_id alone to the token endpoint, and registers a loopback redirect such as http://127.0.0.1/callback (any port) or a private-use scheme such as com.example.app:/callback. The user sees the consent page every time. A web page cannot be a client: neither the token endpoint nor /v1/ answers a cross-origin preflight.
When the user removes your app
The user can remove your app at any time in Connected apps, or by uninstalling it if they installed it, and its next call fails with 401. When the user signs out of your app, revoke its refresh token at the revocation endpoint, which ends that install's tokens.
When the token expires
After an hour, /v1/ answers 401 with the code unauthorized and an error beginning invalid_token. Exchange again when a call gets 401, or shortly before expires_in runs out, and retry the call once. If the exchange itself fails with invalid_client or invalid_scope, the client or its secret has been deleted, revoked or narrowed: stop and fix it on the Integrations page, because retrying cannot succeed. Keep the token between calls: the token endpoint limits exchanges per client and per address, and a program that exchanges on every call is refused with 429 and rate_limited within the hour (wait for Retry-After). This grant has no refresh token: the secret is exchanged again.
Access tokens reach only the API routes
An access token works only on routes under /v1/. Sent to any other Lingara route it is refused with 401, and the body begins api_token_not_accepted, as plain text or inside an error field, never in the {code, error} envelope the /v1/ routes use. Send the access token in the Authorization header, as below.
curl "https://api.getlingara.com/v1/usage" \
-H "Authorization: Bearer $LINGARA_TOKEN"Scopes
Each operation needs exactly one scope, named on its page. An access token without that scope is refused with 403 and the code insufficient_scope. To call the operation, ask for its scope at the exchange, if the client is allowed it. The table below lists each scope and the operations it allows.
vocab:generate | Generate vocabulary lists. | Generate a vocabulary list |
lesson_plans:read | Read your lesson plans and reconnect to their progress. | Get a lesson planReconnect to a lesson plan |
lesson_plans:write | Create lesson plans. | Create a lesson plan |
tutor:converse | Hold tutor conversations. Requires a paid plan. | Send a tutor turn |
usage:read | See your remaining allowance, or a `metered` client's usage this month. | Get your remaining allowance |
events:read | Read events about your account, and register endpoints that receive them. | List eventsStream events |
events:write | Send events from your game or integration to Lingara. | Send an event |
Who pays for a call
A client is billed in one of two ways, chosen when it is created. An allowance client spends its owner's allowance, the same allowance as your apps, and its access tokens spend it too; GET /v1/usage shows what is left. A metered client spends no allowance: it is billed per credit through a usage-billing subscription you set up on the Integrations page. Its calls are refused with 402 and spend_cap_reached once the client or your account reaches its monthly spending limit, and with 402 and metered_billing_inactive while usage billing is not active. A lesson plan is 10 credits, a tutor turn 1 credit and a vocabulary generation 3 credits, so the units that GET /v1/usage reports convert to credits at those weights. A call an app makes for another user, with a token from an authorization code, always spends that user's allowance, whatever the client's mode.
Keep secrets and tokens on a server
The client secret belongs on a server you control, never in a web page, a browser extension or an app bundle, where anyone can read it. A native app is a public client and holds no secret. Neither /v1/ nor the token endpoint answers a cross-origin preflight, so a browser on another site cannot call them anyway.
Manage clients
On the Integrations page, at app.getlingara.com/admin, create, rename and delete clients, change what each one can do and which version it is pinned to, and create and revoke its secrets. Deleting a client stops every access token it holds at once, and ends every user's grant of it. Revoking a secret stops, at once, every access token that secret was exchanged for. Narrowing a client's scopes also applies at once; widening them, or changing its version, applies from the next exchange.
Rotate a secret
A client can hold two secrets at once. To rotate, create a new secret, deploy it, and revoke the old one once its Last used date on the Integrations page stops changing. A process that already has the new secret but still holds a token from the old one gets one 401 and exchanges again. A client's only secret cannot be revoked: create its replacement first, or delete the client.