guides

Webhooks

Have wxrks call your server when projects, tasks, work units, org units or users change, instead of polling for changes.

Create a webhook

Create it in the app under Settings โ†’ Account settings โ†’ Webhooks, or with Create Webhook:

{
  "url": "https://example.com/wxrks/webhooks",
  "type": "WORK_UNIT_STATUS_CHANGE",
  "secret": "<a long random value>"
}

Right after you create it, wxrks sends a validation request to the URL. The webhook becomes active when your server answers it with any 2xx status within 3 seconds; wxrks tries up to three times. Otherwise the webhook stays inactive: fix the endpoint and create the webhook again. The validation request has this body:

{
  "event_type": "WEBHOOK_VALIDATION",
  "message": "This is a test event to validate your webhook endpoint",
  "timestamp": 1791674000000,
  "webhook_uuid": "<webhook uuid>",
  "attempt": 1,
  "events": ["WORK_UNIT_STATUS_CHANGE"],
  "expected_response": "Please respond with an HTTP 2xx status to activate this webhook"
}

Turn on events for your org units

Project, task, work unit and org unit events are sent only for org units where Enable Webhook Events is on, in the org unit's Notifications tab. It is off by default. Organization and user events do not depend on it.

Event types

typeSent when
NEW_PROJECTA project is created.
NEW_EMPTY_PROJECTAn empty project is created.
PROJECT_STATUS_CHANGEA project's status changes.
PROJECT_UPDATEA project is updated.
PROJECT_INGESTION_COMPLETEDA project's files are ingested.
PROJECT_TRANSLATION_FINISHEDA project's translation is finished.
TASK_ASSIGNEDA task is assigned.
TASK_STATUS_CHANGEA task's status changes.
TASK_UPDATEA task is updated.
WORK_UNIT_STATUS_CHANGEA work unit's status changes.
WORK_UNIT_TRANSLATION_FILE_READYA work unit's translated file is ready.
WORK_UNIT_IMPORT_FINISHEDAn import of a bilingual file into a work unit ends, with IMPORTED or ERROR.
TRANSLATION_KIT_JOB_FINISHEDA translation kit job ends, with COMPLETED or FAILED: an export kit, translated files, source files, or a bulk import.
NEW_ORG_UNIT, ORG_UNIT_UPDATEAn org unit is created or updated.
NEW_ORG, ORG_UPDATEAn organization is created or updated.
NEW_USER, USER_UPDATEA user is created or updated.

What a request looks like

Every request is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json; charset=UTF-8
User-Agentwxrks
X-BWX-Event-IDThe event's id. Retries keep it.
X-BWX-Webhook-IDThe id of the webhook, which tells you the event type.
X-BWX-TimestampWhen the request was signed, in Unix seconds. Only when the webhook has a secret.
X-BWX-Signaturev1= followed by the lowercase hex HMAC-SHA256 of {X-BWX-Timestamp}.{raw body}, keyed with the webhook's secret. Only when the webhook has a secret.
X-BWX-Webhook-SecretThe webhook's secret in plain text, when it has one. Kept for receivers written before signatures; check the signature instead.

Verifying the signature

Give the webhook a secret when you create it, then accept a request only when its signature matches:

  1. Read the raw body exactly as received, before parsing the JSON.
  2. Reject the request when X-BWX-Timestamp or X-BWX-Signature is missing, or when the timestamp is more than 5 minutes away from your clock.
  3. Compute the HMAC-SHA256 of the timestamp, a dot (.), and the raw body, with your secret as the key (UTF-8). Write it in lowercase hexadecimal and put v1= in front.
  4. Compare it with X-BWX-Signature in constant time.
const crypto = require('crypto')

function isValidWxrksSignature (rawBody, timestamp, signature, secret) {
  if (!timestamp || !signature || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false
  }
  const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'))
  const received = Buffer.from(signature)
  return expected.length === received.length && crypto.timingSafeEqual(expected, received)
}

To test your implementation: with the secret s3cret, the timestamp 1700000000 and the body {"id":1}, the signature is v1=ee0658aa4e37018df69c24227df01e0f680eb3b87c7f1f9bd936e283cfe01d9b.

Every attempt, retries included, is signed again with a new timestamp. A secret cannot be changed on an existing webhook: to rotate it, create a new webhook with the new secret, accept both secrets while you switch, then delete the old webhook.

The body describes the project, task, work unit, org unit or user the event is about; its fields depend on the event type. wxrks adds a few fields for some events:

EventsAdded fields
events about a continuous projectci_tag: the project's CI tag
PROJECT_STATUS_CHANGE, PROJECT_INGESTION_COMPLETEDworkflows, target_locales
WORK_UNIT_STATUS_CHANGE, WORK_UNIT_TRANSLATION_FILE_READYworkflow, is_last_workflow
WORK_UNIT_STATUS_CHANGEprevious_status and new_status: status codes, such as READY_TO_TRANSLATE and DELIVERED
WORK_UNIT_TRANSLATION_FILE_READYdetails.download: the work unit download call, which returns a link to the translated file
WORK_UNIT_IMPORT_FINISHEDworkflow, is_last_workflow; details.import: the import call
TRANSLATION_KIT_JOB_FINISHEDdetails.job: the job call, which returns the download link once the job has completed
TASK_ASSIGNED, TASK_STATUS_CHANGEname and reference of the task's project
TASK_STATUS_CHANGE, PROJECT_STATUS_CHANGEprevious_status and new_status: status codes, such as PENDING_ACCEPTANCE and DELIVERED

Import and export events

These two events end a wait on an import or a job, so you need not poll.

WORK_UNIT_IMPORT_FINISHED carries the import: id (the work unit), import_id, project_uuid, org_unit_uuid, import_type (XLIFF or XLSX), status (IMPORTED or ERROR), error (the reason code, or null), the segment counts segments_in_file, segments_without_id, segments_updated, segments_confirmed and segments_locked, and creation_date and ending_date in milliseconds. The fields mean the same as in Get Work Unit Import.

TRANSLATION_KIT_JOB_FINISHED carries the job: id (the job), project_uuid, project_name, org_unit_uuid, translation_kit_type (LOC_KIT for an export kit, TRANSLATED_FILES, SOURCE_FILES, IMPORT_XLIFF or IMPORT_XLSX for a bulk import), status (COMPLETED or FAILED), creation_date and ending_date in milliseconds, and work_unit_uuids when the job was limited to some work units. It carries no download link: call details.job for one.

Answering and retries

Answer with a 2xx status within 3 seconds, then do any slow work: wxrks does not wait longer.

The Events tab next to Webhooks in the app lists recent deliveries with the status your server answered, and can retry a delivery. A delivery retried from there carries an X-BWX-Retry-Of header with the id of the original delivery, and its body gains retryOf and retryRequestedAt.