REST API¶
Pontoon provides a set of RESTful endpoints via the Django REST Framework, accessible under /api/v2/.
Authentication¶
Most endpoints are publicly accessible and require no authentication. A few endpoints require an authenticated user.
Requests can be authenticated either with a session cookie or with a Personal Access Token (PAT). Session requests that write data are subject to Django's CSRF checks. The upload endpoints accept both; POST /api/v2/pretranslate/, which returns a machine pretranslation for a string, accepts only a PAT. You can create a PAT from your user settings page (see the User Accounts & Settings documentation for details).
Send the token in the Authorization header using the Bearer scheme:
A PAT automatically expires one year after it is created, and can be deleted manually at any time. Requests made with an invalid or expired token are rejected.
JSON Mode¶
When a request is sent without any headers or with Accept: application/json,
the endpoint will return JSON application/json responses to GET requests.
An example GET request may look like this:
Browsable API¶
When accessed from a browser, Pontoon’s REST API provides a browsable, interactive HTML interface powered by Django REST Framework.
Available at any /api/v2/ endpoint, the browsable API lets you:
- View and explore JSON data in a human-friendly format
- See validation rules and error messages inline
- Navigate related resources easily via hyperlinks
This interface is especially useful for exploring the API without external tools like Postman or curl.
Response Customization¶
You can customize the response by specifying the fields you want to include using the fields=field_1,field_2,...field_N query parameter. This allows you to limit the data returned to only the fields you need, reducing payload size and improving performance.
For example, to retrieve only the name and code fields for locales, you can use:
This will return a response containing only the specified fields for each locale.
Pagination¶
All list-based endpoints are paginated. By default, each page contains up to 100 items.
Use the ?page=N query parameter to navigate between pages.
An example may look like this:
The page size can also be set with the ?page_size=N query parameter, reaching a maximum of 1000 items.
An example may look like this:
Terminology Matching¶
GET /api/v2/terminology/matches/¶
Find the terms appearing in a text, with their translation in a given locale.
Unlike /api/v2/search/terminology/, which looks up terms by name, this
endpoint matches every known term against the text, at word boundaries: a term matches
the start of a word, so open matches Opened, but not Reopened. Terms without a
definition, and terms marked as forbidden, are never returned.
| Parameter | Description |
|---|---|
locale |
Locale code |
text |
Text to match terms against |
$ curl --globoff \
--data-urlencode "locale=it" \
--data-urlencode "text=Open a new tab" \
--get "https://example.com/api/v2/terminology/matches/"
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"definition": "Allow access",
"part_of_speech": "verb",
"text": "open",
"translation_text": "apri",
"usage": "Open the door.",
"notes": ""
},
{
"definition": "A page in the browser",
"part_of_speech": "noun",
"text": "tab",
"translation_text": "scheda",
"usage": "Open a new tab.",
"notes": ""
}
]
}
translation_text is null for terms not yet translated in the locale, and the term
itself for terms marked as "do not translate", such as product names.
No authentication is required. Texts over the maximum length are rejected with 400:
the limit is configurable via TERMINOLOGY_API_MAX_CHARS (default 2048 characters).
An unknown locale returns 404.
The endpoint is rate limited per user, or per IP address for anonymous requests, with a
burst limit of 60 calls per minute and a sustained limit of 600 calls per hour by default
(configurable via API_TERMINOLOGY_THROTTLE_BURST and
API_TERMINOLOGY_THROTTLE_SUSTAINED). Calls over the limit are rejected with 429.
The two limits are not independent: calls rejected by the burst limit still count against
the sustained limit, so a client that keeps calling after a 429 spends its hourly quota
on rejected calls. For example, 60 accepted calls followed by 540 rejected ones exhaust
the hourly quota, locking the client out for one hour. This quota is separate from the one
used by the write endpoints.
Write Endpoints¶
The following endpoints can write data and always require authentication, with a Personal Access Token or with a session cookie and CSRF token.
POST /api/v2/upload/translations/¶
Update translations from an uploaded translation file, as the authenticated user. This is also used by the Upload Translations button in the translate app.
The request body is multipart/form-data with these fields:
| Field | Description |
|---|---|
project |
Project slug |
locale |
Locale code |
resource |
Resource path within the project |
uploadfile |
Translation file, in the same format as the target resource |
$ curl -X POST \
-H "Authorization: Bearer <YOUR-TOKEN>" \
-F "project=firefox" \
-F "locale=it" \
-F "resource=browser/browser.ftl" \
-F "uploadfile=@browser.ftl" \
"https://example.com/api/v2/upload/translations/"
A successful request returns a summary of the import:
{
"updated": 12,
"unchanged": 3,
"undefined_keys": [["obsolete_key"]],
"undefined_keys_count": 1,
"badge_updates": [{ "name": "Translation Champion", "level": 2 }]
}
updated: translations added or replaced. Uploaded translations replace the current approved translation, or approve a matching suggestion.unchanged: translations identical to the current approved, pretranslated or fuzzy one, ignored.undefined_keys: keys of translations with no matching string in Pontoon, ignored. Each key is a list of strings, in the same format as thekeyfield of entities. Only the first 100 keys are listed.undefined_keys_count: total number of keys with no matching string in Pontoon, before truncation.badge_updates: badges whose level the upload raised, each with itsnameand newlevel. The user is also notified of each. Empty when no level changed.
The upload is additive: strings missing from the uploaded file are left untouched, so
partial files can be used to update a subset of translations. Re-uploading an unchanged
file succeeds with "updated": 0. A file that cannot be parsed, or that contains no
translations at all, is rejected with 400 rather than reported as an unchanged upload,
so "updated": 0 always means the file was valid and simply didn't change anything.
Requirements and limits:
- You must have translator rights for the target locale, and the project locale must not
be read-only. Otherwise the request is rejected with
403. - The project must not be disabled, and both the project and the resource must be
enabled for the target locale. Otherwise the request is rejected with
404. - Uploaded files must be under 5000 kB, and must match the format of the target resource.
- The endpoint is rate limited per user, with a burst limit of 30 calls per minute and a
sustained limit of 180 calls per hour by default (configurable via
API_UPLOAD_THROTTLE_BURSTandAPI_UPLOAD_THROTTLE_SUSTAINED). The two limits are not independent: calls rejected by the burst limit still count against the sustained limit, so a client that keeps calling after a429spends its hourly quota on rejected calls. For example, 30 accepted uploads followed by 150 rejected ones exhaust the hourly quota, locking the client out for one hour.
Uploaded translations are written to the database immediately, and pushed to the project's VCS repository by the next sync.
Status codes:
| Code | Meaning |
|---|---|
200 |
Upload accepted (possibly with "updated": 0) |
400 |
Missing or invalid field, unsupported format, unparseable or empty file, or file too large |
403 |
Not authenticated, invalid or expired token, missing CSRF token, or insufficient permission |
404 |
Unknown or disabled project, unknown locale or resource, or project or resource not enabled for the locale |
409 |
A concurrent upload or review changed the same translations; retry the request |
429 |
Rate limit exceeded |
POST /api/v2/upload/pretranslations/¶
Store translations from an uploaded translation file as pretranslations.
This API requires the user to be a member of the pretranslators group,
in addition to have translator rights for the target locale and project.
The request body is the same multipart/form-data as
POST /api/v2/upload/translations/:
$ curl -X POST \
-H "Authorization: Bearer <YOUR-TOKEN>" \
-F "project=firefox" \
-F "locale=it" \
-F "resource=browser/browser.ftl" \
-F "uploadfile=@browser.ftl" \
"https://example.com/api/v2/upload/pretranslations/"
A successful request returns a summary of the import:
{
"created": 8,
"replaced": 2,
"converted": 1,
"unchanged": 3,
"skipped": 12,
"failed_checks": [
{ "key": ["entity_key"], "errors": ["Double space"], "warnings": [] }
],
"failed_checks_count": 1,
"undefined_keys": [["obsolete_key"]],
"undefined_keys_count": 1,
"badge_updates": []
}
created: number of pretranslations added for strings with no pretranslation or fuzzy translation.replaced: number of pretranslations replacing a previous, different pretranslation or fuzzy translation. The replaced translation is rejected.converted: number of existing translations made the active pretranslation, because they match the uploaded translation. Their original author is preserved.unchanged: number of translations identical to the current pretranslation, ignored.skipped: number of strings left untouched, because they already have an approved translation, or are marked as fuzzy in the uploaded file.failed_checks: number of strings left untouched, because the uploaded translation fails quality checks. Each entry has thekeyof the string, in the same format as thekeyfield of entities, and theerrorsandwarningsreported for it. Only the first 100 keys are listed.failed_checks_count: total number of strings left untouched because of failing checks, before truncation.undefined_keys: keys of translations with no matching string in Pontoon, ignored. Each key is a list of strings, in the same format as thekeyfield of entities. Only the first 100 keys are listed.undefined_keys_count: total number of keys with no matching string in Pontoon, before truncation.badge_updates: badges whose level the upload raised, each with itsnameand newlevel. The user is also notified of each. Empty when no level changed.
Unlike the built-in pretranslation, strings with unreviewed suggestions are pretranslated. Suggestions that don't match the uploaded translation are kept as unreviewed suggestions. Fuzzy translations already in Pontoon are rejected and replaced by the pretranslation, while approved translations are never replaced. An uploaded translation that fails any quality check is neither stored nor converted, and the previous translation is left in place, so that a broken translation never replaces a good one.
The requirements, limits and status codes of
POST /api/v2/upload/translations/ also apply here,
with one difference: the request is rejected with 403 unless the user is a member of
the pretranslators group.
POST /api/v2/upload/suggestions/¶
Store translations from an uploaded translation file as unreviewed suggestions, authored by the authenticated user.
The request body is the same multipart/form-data as
POST /api/v2/upload/translations/:
$ curl -X POST \
-H "Authorization: Bearer <YOUR-TOKEN>" \
-F "project=firefox" \
-F "locale=it" \
-F "resource=browser/browser.ftl" \
-F "uploadfile=@browser.ftl" \
"https://example.com/api/v2/upload/suggestions/"
A successful request returns a summary of the import:
{
"created": 9,
"restored": 1,
"unchanged": 4,
"failed_checks": [
{
"key": ["entity_key"],
"errors": ["Ending newline mismatch"],
"warnings": []
}
],
"failed_checks_count": 1,
"undefined_keys": [["obsolete_key"]],
"undefined_keys_count": 1,
"badge_updates": []
}
created: number of suggestions added.restored: number of rejected translations matching the upload that were un-rejected, becoming pending suggestions again.unchanged: number of uploaded translations that match existing unrejected translations, in any review state, ignored.failed_checks: strings left untouched, because the uploaded translation has errors. Each entry has thekeyof the string, in the same format as thekeyfield of entities, and theerrorsandwarningsreported for it. Only the first 100 keys are listed.failed_checks_count: total number of strings left untouched because of errors, before truncation.undefined_keys: keys of translations with no matching string in Pontoon, ignored. Each key is a list of strings, in the same format as thekeyfield of entities. Only the first 100 keys are listed.undefined_keys_count: total number of keys with no matching string in Pontoon, before truncation.badge_updates: badges whose level the upload raised, each with itsnameand newlevel. The user is also notified of each. Empty when no level changed.
Nothing already in Pontoon is replaced or rejected: every uploaded translation is stored as a suggestion, unless the string already has an unrejected translation with the same value, whether approved, pretranslated or unreviewed. The fuzzy flag of the uploaded file is ignored: the translation is stored as a plain suggestion.
A rejected translation matching the upload is un-rejected instead of being suggested again, so a translation rejected by mistake can be re-proposed. Its original author and date are kept, and it becomes a pending suggestion, awaiting review like any other.
Uploaded translations reported with errors are left out, as the editor rejects them as well. Translations with warnings are stored, with their warnings, as a reviewer can still accept them.
NOTE: unlike in the UI, where any user can submit suggestions, this endpoint requires translator rights. This is done to prevent abuse, since a malicious actor could submit thousands of suggestions.
The requirements, limits and status codes of
POST /api/v2/upload/translations/ also apply here.