Skip to content

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.

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.

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:

typeHow to connect
formPOST /login with the listed fields as credentials.
oauthPOST /oauth-url → a person opens the link → POST /oauth-complete. Or, if you already hold a token, POST /login with it.
// 1. Discover the fields
const { fields } = await fetch('https://your-app.com/login-fields', {
headers: { Authorization: `Bearer ${jwtToken}` },
}).then(r => r.json());
// 2. Connect
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

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-fields
const { 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 pending
let 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/expired

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 Content

A 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.

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.

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):

FieldTypeDescription
crowdinTokenstringA 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.
integrationCredentialsstring | objectIntegration 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.

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:

FeatureWhy it needs a stored connection
Scheduled (cron) syncRuns on a timer with no incoming request to carry the credentials.
Webhook deliveryIncoming webhooks have no integrationCredentials in their body.
Webhook registrationRegistration is tied to the stored connection’s lifecycle.
Job resume after a restartA persisted job rehydrates from stored credentials, not the original request.
Token refreshRefreshing an expired token updates the stored record, which is absent here.
isNew new-content detectionThe content baseline is captured when the integration is connected.
POST /sync-settingsIt persists an auto-sync selection against the stored connection.

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.

Declare in IntegrationLogic whether your app handles entryIds filtering:

ValueBehavior
trueTrusted — the 501 guard is skipped.
falseOpted out — returns 501 immediately when entryIds is supplied.
undefinedAuto-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);
},
// ...
};

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.

To find a Crowdin file by its integration ID without fetching the full tree:

GET /crowdin-files?externalId=cam_1

Returns a single file object ({ id: "102", externalFileId: "cam_1" }) or 404.

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=true

isNew 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 isNew flag, 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.

// 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 content
const 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 Crowdin
const 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 completes
let 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 IDs
const 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 back
await 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.