Transifex lets you specify a webhook and get notified whenever a target language of a resource has fully been translated, reviewed, or proofread, as well as when translation fill-ups are done. This way, you can pull translations as soon as they are ready without constantly checking Transifex for updates.
Adding webhooks to a project
📝Note: For webhooks to work, you'll need a web server to listen for the webhook calls and an application to react to those. The webhooks are triggered automatically in Transifex.
Webhooks can be added to any project in an Organization. To do this:
From the Dashboard, head to the project you want to set up webhooks for
Click on Settings. Only Project Maintainers and Organization Admins can access this menu.
In the submenu, click on Webhooks.
Click on Add Webhook.
In the popup, enter your webhook URL and secret key (the secret key is optional). Then, choose whether you'd like to be notified about all events or only a specific one.
The callback URL should listen for a POST request with the following variables:
project: The slug of the project this notification is for
resource: The slug of the resource this notification is for
language: The language code of the translation that was modified
translated: An integer representing the completion percentage of translations for the particular resource in the specific language. If a resource is fully reviewed or proofread in a particular language, a variable named reviewed or proofread, respectively, will be used instead of the translated one
event: A string that describes the webhook type, which you can use to filter webhooks. The possible values are:
translation_completed
When a target language of a resource is 100% translated, then the webhook will be triggered. The following data will be returned:
{
"project": "project_slug",
"translated": 100,
"resource": "resource_slug",
"event": "translation_completed",
"language": "lang_code"
}
review_completed
When a target language of a resource is 100% reviewed, then the webhook will be triggered. The following data will be returned:
{
"resource": "resource_slug",
"language": "lang_code",
"reviewed": 100,
"project": "project_slug",
"is_final": true/false,
"event": "review_completed"
}
📝Note: The parameter "is_final" is false if the second review step (proofread) has been enabled and only the first review step has been completed.
proofread_completed
When a target language of a resource is 100% proofread, then the webhook will be triggered. The following data will be returned:
{
"resource": "resource_slug",
"language": "lang_code",
"reviewed": 100,
"project": "project_slug",
"is_final": true/false,
"event": "proofread_completed"
}
fillup_completed
When TM or MT fill-up tasks are completed.
{
"machine translation": 0,
"resource": "resource_slug",
"language": "lang_code",
"project": "project_slug",
"translated": 50,
"translation memory": 50,
"event": "fillup_completed"
}
translation_updated_completed
When a 100% translated resource has a translation updated (edited).
{
"project": "project_slug",
"translated": 100.0,
"resource": "resource_slug",
"event": "translation_completed_updated",
"language": "lang_code"
}
task_tag_created
When a set of strings is selected in the editor, and the ‘Create Task’ button is pressed, the webhook will be triggered. The following data will be returned:
{
"project": "project_slug",
"language": "lang_code",
"tag": "tag_name",
"url": "https://example.com/org_slug/project_slug/translate/lang_code/resource_slug?q=tags%3AtxtaskENqwertyuiop12",
"event": "task_tag_created"
}
task_tag_completed
When a set of strings for which a task has already been created is fully translated, the webhook will be triggered. The following data will be returned:
{
"project": "project_slug",
"language": "lang_code",
"tag": "tag_name",
"url": "",
"event": "task_tag_completed"
}
resource_language_stats
This webhook type is exclusively accessible via the API and not available in the UI. It is triggered in response to the following events:
- source_edit: update due to source string edits
- translated: update after string translation
- untranslated: update when a previously translated string was marked as untranslated
- reviewed: update after string review
- unreviewed: update when a previously reviewed string is reverted
- proofread: update after string proofreading
- unproofread: update when a previously proofread string is reverted
For each update that occurs at the string level, the webhook will return a response like the following one:
{
"action": "updated",
"after": {
"total_strings": 462,
"total_words": 2367,
"translated_strings": 1,
"translated_words": 1,
"translated_strings_percentage": 0.21,
"reviewed_strings": 1,
"reviewed_words": 1,
"reviewed_strings_percentage": 0.21,
"proofread_strings": 1,
"proofread_words": 1,
"proofread_strings_percentage": 0.21,
"completed_strings": 1,
"completed_words": 1,
"completed_strings_percentage": 0.21
},
"before": {
"total_strings": 462,
"total_words": 2367,
"translated_strings": 2,
"translated_words": 11,
"translated_strings_percentage": 0.44,
"reviewed_strings": 2,
"reviewed_words": 11,
"reviewed_strings_percentage": 0.44,
"proofread_strings": 1,
"proofread_words": 1,
"proofread_strings_percentage": 0.21,
"completed_strings": 1,
"completed_words": 1,
"completed_strings_percentage": 0.21
},
"event": "resource_language_stats",
"happened_at": "1698255953.624257",
"language": {
"code": "lang_code"
},
"organization": {
"slug": "organization_slug",
"unique_identifier": "organizations:prY5am32QZdJbW7Z"
},
"project": {
"slug": "project_slug",
"unique_identifier": "projects:M97wXPx9rLLyzjKl"
},
"proofread": false,
"resource": {
"slug": "resource_slug",
"unique_identifier": "resources:MLgZbd5BJram6rWq"
},
"reviewed": false,
"source_edit": false,
"translated": false,
"unproofread": false,
"unreviewed": true,
"untranslated": true
}
6. When you're done, click Save changes.
When the event(s) you specified happens, an update is fired via a POST request from Transifex to the provided URL. The body of the post will be a JSON payload of the variables mentioned above.
Headers
If you've defined a secret key, then each webhook will include two headers:
X-TX-Signature: This is the computed signature generated by Transifex. It's used to tell whether the request is valid or not.
User-Agent: Transifex itself.
📝Note: The difference between x-tx-signature and x-tx-signature-v2 is that x-tx-signature is kept for backward compatibility.
Verifying a webhook
The latest version of the Transifex webhook provides an extensible way to notify third-party services of changes in the progress of a resource in Transifex.
To validate that the webhook is coming from Transifex, you must first set a shared secret with Transifex. This will be used to calculate the webhook signature x-tx-signature-v2. When the webhook is received, we calculate the signature on our end and check if it matches the submitted signature.
To calculate the signature, you need to concatenate (one element per line) the following:
The method type, e.g., POST
The URL path where your webhook listener listens
The submission date (taken from the
Date
header)The hash of the contents of the webhook calculated through md5
After that, the signature is extracted by encrypting the concatenated data with the SHA256 algorithm using the shared secret. Finally, we encode the calculated hash in Base64.
Here's a sample of the webhook content:
{
"project": <project slug>,
"translated": <completion percentage>,
"resource": <resource slug,
"event": "translation_completed" || “review_completed” || “proofread_completed“ || “fillup_completed”,
"language": <language_code>
}
Below are a series of code samples that demonstrate the required validation procedure.
PHP
$http_verb = 'POST';
$received_json = file_get_contents("php://input", TRUE);
$webhook_sig = $_SERVER['X-TX-Signature-V2'];
$http_url_path = $_SERVER['X-TX-Url'];
$http_gmt_date = $_SERVER['Date'];
$content_md5 = md5($received_json);
$msg = join(PHP_EOL, array('POST', $http_url_path, $http_gmt_date, $content_md5));
$sig = base64_encode(hash_hmac('sha256', $msg, TRANSIFEX_SECRET, true));
return $sig == $webhook_sig
Node
const crypto = require('crypto');
const httpVerb = 'POST';
const httpUrlPath = 'http://www.test.com/page/';
const httpGmtDate = 'Fri, 31 May 2024 11:42:12 GMT';
const secret = 'secret_key';
const content = '{"event": "translation_completed_updated", "language": "lang_code", "project": "project-slug", "resource": "resource-slug", "translated": 100}';
// Compute MD5 hash
const contentMd5 = crypto.createHash('md5').update(content).digest('hex');
// Concatenate the elements
const data = [httpVerb, httpUrlPath, httpGmtDate, contentMd5].join('\n');
// Compute HMAC SHA256 signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(data);
const txSignature = hmac.digest('base64');
console.log('Calculated Signature:', txSignature);
Python
import base64
import hmac
import hashlib
import json
# Both http_gmt_date and http_url_path can be found through the response headers namely in the HTTP_DATE and HTTP_X_TX_URL headers respectively
http_verb = 'POST'
http_url_path = 'http://www.test.com/page/'
http_gmt_date = 'Fri, 31 May 2024 11:42:12 GMT'
secret = 'secret_key'
response_payload = { "event": "translation_completed_updated", "language": "lang_code", "project": "project_slug", "resource": "resource_slug", "translated": 100 }
# Convert payload to JSON string and then to bytes
payload_json = json.dumps(payload)
payload_encoded = payload_json.encode('utf-8')
content_md5 = hashlib.md5(payload_encoded).hexdigest()
msg = b'\n'.join([
http_verb, http_url_path, http_gmt_date, content_md5
]).encode('utf-8')
tx_signature = base64.b64encode(
hmac.new(
key=secret.encode('utf-8'),
msg=msg,
digestmod=hashlib.sha256
).digest()
).decode('utf-8')
Ruby
require 'openssl'
require 'base64'
require 'digest'
HMAC_DIGEST_256 = OpenSSL::Digest.new('sha256')
# Both http_gmt_date and http_url_path can be found through the response headers namely in the HTTP_DATE and HTTP_X_TX_URL headers respectively
http_verb = 'POST'
http_url_path = 'http://www.test.com/page/'
http_gmt_date = 'Fri, 31 May 2024 11:42:12 GMT'
secret = 'secret_key'
content = '{"event": "translation_completed_updated", "language": "lang_code", "project": "project-slug", "resource": "resource-slug", "translated": 100}'
# Compute MD5 hash
content_md5 = Digest::MD5.hexdigest(content)
data = [http_verb, http_url_path, http_gmt_date, content_md5].join("\n")
tx_signature = Base64.encode64(
OpenSSL::HMAC.digest(HMAC_DIGEST_256, secret, data)
).strip
📝Note: For the Ruby code, the structure of the value for the variable content you will receive from the webhook should have the following structure as STRING:
The payload begins with an opening curly brace
{
Each key-value pair is separated by a colon
:
with a spaceThe keys are enclosed in double quotation marks
""
The values can be strings or numbers, which are also enclosed in double quotation marks for strings.
Each key-value pair is separated by a comma
,
followed by a space.The payload ends with a closing curly brace
}
There should not be a comma after the value in the last key-value pair, as it is the last element in the payload.
The code you see in the above Ruby example, with the single quotes,
content = '{"event": "translation_completed_updated", "language": "lang_code", "project": "project-slug", "resource": "resource-slug", "translated": 100}'
is to clarify that the value for the variable content is STRING.
💡Tip
Looking for more help? Get support from our Transifex Community Forum!
Find answers or post to get help from Transifex Support and our Community.