# 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

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.

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

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

```http
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

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

```typescript
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

`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

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

## Full Round-Trip Example

```js
// 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,
  }),
});
```

**Security:** `crowdinToken` and `integrationCredentials` are validated and stripped from `req.body` before any handler sees them. `crowdinToken` is rejected with `403` if it does not belong to the same organization as the JWT.