API Usage
This page describes how AI agents and automated pipelines can drive the full translation workflow — syncing content to Crowdin, waiting for translation, and pushing translations back — entirely through API calls, without any prior human login or UI interaction.
Authentication
Section titled “Authentication”All API calls carry a JWT from Crowdin’s proxy that proves the request’s origin. The moduleKey configuration further restricts access so only designated Crowdin modules can call your endpoints. Supplying valid credentials (see below) does not bypass these checks.
Connect an integration
Section titled “Connect an integration”Before an agent can sync anything, the integration has to be connected for the project. A connection stores the integration credentials server-side, exactly as a UI login does — after it, every endpoint works from the stored credentials with no per-request integrationCredentials.
Start with GET /login-fields. Its type tells you which flow to use:
type | How to connect |
|---|---|
form | POST /login with the listed fields as credentials. |
oauth | POST /oauth-url → a person opens the link → POST /oauth-complete. Or, if you already hold a token, POST /login with it. |
Form integrations
Section titled “Form integrations”// 1. Discover the fieldsconst { fields } = await fetch('https://your-app.com/login-fields', { headers: { Authorization: `Bearer ${jwtToken}` },}).then(r => r.json());
// 2. Connectawait fetch('https://your-app.com/login', { method: 'POST', headers: { Authorization: `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, credentials: { email: 'user@example.com', password: 'secret' }, }),}); // 204 No ContentOAuth integrations — authorization link
Section titled “OAuth integrations — authorization link”An agent cannot click through a consent screen, so it asks for a link and hands it to a person.
// 1. Start the flow — loginForm carries any fields reported by /login-fieldsconst { url, expiresAt } = await fetch('https://your-app.com/oauth-url', { method: 'POST', headers: { Authorization: `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, loginForm: {} }),}).then(r => r.json());
// 2. Show `url` to the person who asked for the connection, then poll until it is no longer pendinglet res;do { await new Promise(r => setTimeout(r, 3000)); res = await fetch('https://your-app.com/oauth-complete', { method: 'POST', headers: { Authorization: `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123 }), });} while (res.status === 202); // 202 = still pending; 204 = connected; 4xx = failed/expiredOAuth integrations — token in hand
Section titled “OAuth integrations — token in hand”If the agent already holds an OAuth access token, it can skip the browser step entirely:
await fetch('https://your-app.com/login', { method: 'POST', headers: { Authorization: `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, credentials: { accessToken: 'ya29.a0Af...', refreshToken: '1//0g...', expiresIn: 3600 }, }),}); // 204 No ContentA refresh token only works if this app’s own OAuth client issued it — a token minted by another client cannot be refreshed and the connection will stop working once it expires.
Disconnect
Section titled “Disconnect”POST /logout (body { projectId }) removes the stored credentials for the project. Per-request integrationCredentials are not accepted here — there is no stored connection to remove — and the call returns 400.
Credential Injection
Section titled “Credential Injection”By default, API calls use the Crowdin credentials and integration credentials stored in the database — the ones a user sets up through the UI. For AI agents or other automated callers that manage their own credentials, the framework supports per-request credential injection: supply credentials directly in the request body and the stored credentials are bypassed entirely.
Both fields are optional and only active for API calls (requests that arrive through the API module with isApiCall === true):
| Field | Type | Description |
|---|---|---|
crowdinToken | string | A Crowdin personal access token. Replaces stored organization credentials. Must belong to the same organization as the JWT — rejected with 403 on mismatch. Subscription check is skipped. |
integrationCredentials | string | object | Integration credentials. A plain string is treated as an access token ({ accessToken: value }); an object is passed through as-is. Replaces stored OAuth credentials. checkConnection is called if the integration defines it. |
Integration settings (req.integrationSettings) are still loaded from storage — they represent configuration, not credentials — and default to {} when no settings have been saved yet.
Both fields are stripped from req.body before any handler or integration callback receives the request.
What per-request credentials cannot do
Section titled “What per-request credentials cannot do”Per-request integrationCredentials cover the stateless read and sync endpoints. Anything that runs outside the request or persists state needs a stored connection instead — connect once with POST /login or the OAuth flow (see Connect an integration). The following do not work with per-request credentials:
| Feature | Why it needs a stored connection |
|---|---|
| Scheduled (cron) sync | Runs on a timer with no incoming request to carry the credentials. |
| Webhook delivery | Incoming webhooks have no integrationCredentials in their body. |
| Webhook registration | Registration is tied to the stored connection’s lifecycle. |
| Job resume after a restart | A persisted job rehydrates from stored credentials, not the original request. |
| Token refresh | Refreshing an expired token updates the stored record, which is absent here. |
isNew new-content detection | The content baseline is captured when the integration is connected. |
POST /sync-settings | It persists an auto-sync selection against the stored connection. |
Syncing Files by Entry ID
Section titled “Syncing Files by Entry ID”By default, POST /crowdin-update requires the caller to provide fully-formed file objects. When an agent only knows third-party IDs (e.g., HubSpot campaign IDs), it can pass them directly and let the framework resolve them:
POST /crowdin-update{ "projectId": 12, "crowdinToken": "crowdin-pat-abc", "integrationCredentials": "hubspot-oauth-token-xyz", "entryIds": ["cam_1", "cam_2", "cam_3"]}The framework calls getIntegrationFiles({ ..., entryIds: ["cam_1", "cam_2", "cam_3"] }) and then runs the normal sync path unchanged.
supportsEntryIdSync flag
Section titled “supportsEntryIdSync flag”Declare in IntegrationLogic whether your app handles entryIds filtering:
| Value | Behavior |
|---|---|
true | Trusted — the 501 guard is skipped. |
false | Opted out — returns 501 immediately when entryIds is supplied. |
undefined | Auto-detected — if no returned file ID matches any requested entryId, returns 501. |
const integration: IntegrationLogic = { supportsEntryIdSync: true, // this integration filters by ID in getIntegrationFiles
getIntegrationFiles: async ({ credentials, entryIds }) => { if (entryIds?.length) { // fetch only the requested entries return fetchByIds(credentials, entryIds); } return fetchAll(credentials); },
// ...};External File IDs
Section titled “External File IDs”GET /crowdin-files includes an externalFileId field on each file — the original third-party system ID. This lets agents map between Crowdin file IDs and their source IDs.
Path A — app developer opt-in: Return externalFileId from getCrowdinFiles directly. This is the most accurate approach.
Path B — framework fallback: The framework stores integrationFileId → crowdinFileId mappings during sync and populates externalFileId automatically for apps that haven’t updated.
Reverse lookup
Section titled “Reverse lookup”To find a Crowdin file by its integration ID without fetching the full tree:
GET /crowdin-files?externalId=cam_1Returns a single file object ({ id: "102", externalFileId: "cam_1" }) or 404.
Detecting New Content
Section titled “Detecting New Content”Integration files are returned with an isNew flag so an agent can find content that has appeared but not yet been pushed to Crowdin:
GET /integration-files?isNew=trueisNew is true when a file was created after the integration was first connected and has not yet been synced. The reference point is a per-integration content baseline, captured once on first connect:
- It is never advanced by sync operations — syncing a file is what clears that file’s
isNewflag, not time passing or other files being synced. - It is preserved across reconnects, so refreshing or re-injecting credentials never resurfaces already-synced files as new.
For this to work the integration must return a createdAt timestamp on each file (see Filters). Files without createdAt are reported as isNew: false.
Full Round-Trip Example
Section titled “Full Round-Trip Example”// 0. Connect the integration once (form integration shown; see "Connect an integration" for OAuth).// After this, every call works from the stored credentials — no per-request integrationCredentials.await fetch('https://your-app.com/login', { method: 'POST', headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, credentials: { email: 'user@example.com', password: 'secret' }, }),}); // 204 No Content
// 1. Find files with new contentconst newContent = await fetch('https://your-app.com/integration-files?isNew=true', { headers: { 'Authorization': `Bearer ${jwtToken}` },}).then(r => r.json());
const entryIds = newContent.files.map(f => f.id); // e.g. ['cam_1', 'cam_2']
// 2. Sync sources to Crowdinconst syncResponse = await fetch('https://your-app.com/crowdin-update', { method: 'POST', headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, crowdinToken: 'your-crowdin-pat', entryIds, }),}).then(r => r.json());
const { jobId } = syncResponse;
// 3. Poll until job completeslet done = false;while (!done) { await new Promise(r => setTimeout(r, 3000)); const { data } = await fetch(`https://your-app.com/job-info?jobId=${jobId}`, { headers: { 'Authorization': `Bearer ${jwtToken}` }, }).then(r => r.json()); done = data.status === 'finished';}
// 4. Resolve integration IDs to Crowdin file IDsconst crowdinFiles = await Promise.all( entryIds.map(id => fetch(`https://your-app.com/crowdin-files?externalId=${id}`, { headers: { 'Authorization': `Bearer ${jwtToken}` }, }).then(r => r.json()), ),);const files = Object.fromEntries(crowdinFiles.map(f => [f.id, ['uk', 'de']]));
// 5. Push translations backawait fetch('https://your-app.com/integration-update', { method: 'POST', headers: { 'Authorization': `Bearer ${jwtToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: 123, crowdinToken: 'your-crowdin-pat', files, }),});This example connects once with POST /login and then relies on the stored credentials, so no call carries integrationCredentials. An agent that manages its own credentials can instead inject them per request (see Credential Injection), keeping the limits above in mind.