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.

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.

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.

// 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',
integrationCredentials: 'your-integration-token',
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',
integrationCredentials: 'your-integration-token',
files,
}),
});