# Configuring webhook

> Setup webhook to automatically import Phrase translations back into Uniform

Source: https://docs.uniform.app/docs/integrations/translation-management/phrase/configuring-webhook

Setup webhook to automatically import Phrase translations back into Uniform.

You would need [Phrase](https://docs.uniform.app/docs/integrations/translation-management/phrase) integration to be installed and configured.

> **Webhook has to be publicly available:**
>
> Phrase integration settings has **Webhook URL** input where you need to provide publicly avaiable url
> where Phrase is going to push completed translation job.
>
> You can find a full example of implementation in this [examples repo](https://github.com/uniformdev/examples/tree/main/examples/tms-webhook-phrase)

## Setup

Lets create a webhook that can handle composition or entry translations coming back from Phrase TMS.

1. First we need Phrase and Uniform credentials:

   ```bash
   # Authentication to Phrase
   # https://cloud.memsource.com or https://us.cloud.memsource.com
   PHRASE_API_HOST=
   PHRASE_PROJECT_UID=
   PHRASE_USER_NAME=
   PHRASE_USER_PASSWORD=
   PHRASE_WEBHOOK_SECRET_TOKEN=

   # Authentication to Uniform
   UNIFORM_API_KEY=
   ```

> **Uniform API Keys:**
>
> [Here](https://docs.uniform.app/docs/guides/apis#create-api-key) you can find how to create Uniform API Keys. Phrase integration requires at least **Create** and **Update** permissions for **Compositions / Entries / Assets**

1. We would also need Uniform dependencies

   ```bash
   npm i @uniformdev/canvas @uniformdev/tms-sdk @uniformdev/tms-phrase
   ```
2. Lets validate Phrase webhook

   ```ts
   const token = req.headers['x-memsource-token'];

   if (token !== process.env.PHRASE_WEBHOOK_SECRET_TOKEN) {
       // eslint-disable-next-line no-console
       console.log('Secret token is invalid');
       res.status(403).json({ message: 'Secret token is invalid' });
       return;
   }

   const payload = req.body as PhraseWebhookPayload;

   if (payload.event !== 'JOB_STATUS_CHANGED') {
       // eslint-disable-next-line no-console
       console.log(`skip: event !== 'JOB_STATUS_CHANGED'`);

       res.status(200).json({ updated: false });
       return;
   }

   const job = payload.jobParts.at(0);

   if (!job || !shouldProcessJob(job)) {
       res.status(200).json({ updated: false });
       return;
   }

   const shouldProcessJob = (job: PhraseWebhookPayload['jobParts'][number]): boolean => {
       if (job.status !== 'COMPLETED_BY_LINGUIST') {
           // eslint-disable-next-line no-console
           console.log('skip: job status !== COMPLETED_BY_LINGUIST');

           return false;
       }

       const phraseProjectUid = job.project.uid;

       if (!process.env.PHRASE_PROJECT_UID || phraseProjectUid !== process.env.PHRASE_PROJECT_UID) {
           if (!process.env.PHRASE_PROJECT_UID) {
           // eslint-disable-next-line no-console
           console.log(`skip: missing 'PHRASE_PROJECT_UID' env`);
           } else {
           // eslint-disable-next-line no-console
           console.log(
               `skip: Phrase project id mismatch (expected: ${process.env.PHRASE_PROJECT_UID}, current: ${phraseProjectUid})`
           );
           }

           return false;
       }

       return true;
   };
   ```
3. Lets retrieve target file from the job

   ```ts
   const phraseClient = new PhraseTmsClient({
       apiHost: process.env.PHRASE_API_HOST || assert('missing PHRASE_API_HOST'),
       userName: process.env.PHRASE_USER_NAME || assert('missing PHRASE_USER_NAME'),
       password: process.env.PHRASE_USER_PASSWORD || assert('missing PHRASE_USER_PASSWORD'),
   });

   const translationPayload = job.uid
       ? await phraseClient.downloadTargetFile<TranslationPayload>({
           projectUid: job.project.uid,
           jobUid: job.uid,
       })
       : null;

   if (!translationPayload) {
       // eslint-disable-next-line no-console
       console.log('skip: no translation payload');

       res.status(200).json({ updated: false });
       return;
   }

   function assert(msg: string): never {
       throw new Error(msg);
   }
   ```
4. Now lets merge translation into Uniform entities

_For the US region:_

```ts
const uniformProjectId = translationPayload.metadata.uniformProjectId;
const uniformReleaseId = translationPayload.metadata.uniformReleaseId;
const uniformEntityType = translationPayload.metadata.entityType;
const uniformEntityId = translationPayload.metadata.entity.id;

const canvasClient = new CanvasClient({
    projectId: uniformProjectId,
    apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'),
    bypassCache: true,
});

const contentClient = new ContentClient({
    projectId: uniformProjectId,
    apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'),
    bypassCache: true,
});

const { translationMerged } = await mergeTranslationToUniform({
    canvasClient,
    contentClient,
    translationPayload,
    updateComposition: async ({ canvasClient, composition }) => {
        // eslint-disable-next-line no-console
        console.log('update composition: start');

        await canvasClient.updateComposition(composition);

        // eslint-disable-next-line no-console
        console.log('update composition: done');

        return true;
    },
    updateEntry: async ({ contentClient, entry }) => {
        // eslint-disable-next-line no-console
        console.log('update entry: start');

        await contentClient.upsertEntry(entry);

        // eslint-disable-next-line no-console
        console.log('update entry: done');

        return true;
    },
    onNotFound: ({ translationPayload }) => {
        const entityType = translationPayload.metadata.entityType;
        const entityId = translationPayload.metadata.entity.id;

        // eslint-disable-next-line no-console
        console.log(`skip: can not find ${entityType} (${entityId})`);
    },
    onNotTranslatedResult: ({ updated, errorKind, errorText }) => {
        if (errorKind !== undefined) {
            // eslint-disable-next-line no-console
            console.warn(errorText || 'Unknown error');
        } else if (!updated) {
            // eslint-disable-next-line no-console
            console.log('Translation has no updates');
        }
    },
});
```

_For the EU region:_

```ts
const uniformProjectId = translationPayload.metadata.uniformProjectId;
const uniformReleaseId = translationPayload.metadata.uniformReleaseId;
const uniformEntityType = translationPayload.metadata.entityType;
const uniformEntityId = translationPayload.metadata.entity.id;

const canvasClient = new CanvasClient({
    apiHost: 'https://eu.uniform.app',
    edgeApiHost: 'https://eu.uniform.global',
    projectId: uniformProjectId,
    apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'),
    bypassCache: true,
});

const contentClient = new ContentClient({
    apiHost: 'https://eu.uniform.app',
    edgeApiHost: 'https://eu.uniform.global',
    projectId: uniformProjectId,
    apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'),
    bypassCache: true,
});

const { translationMerged } = await mergeTranslationToUniform({
    canvasClient,
    contentClient,
    translationPayload,
    updateComposition: async ({ canvasClient, composition }) => {
        // eslint-disable-next-line no-console
        console.log('update composition: start');

        await canvasClient.updateComposition(composition);

        // eslint-disable-next-line no-console
        console.log('update composition: done');

        return true;
    },
    updateEntry: async ({ contentClient, entry }) => {
        // eslint-disable-next-line no-console
        console.log('update entry: start');

        await contentClient.upsertEntry(entry);

        // eslint-disable-next-line no-console
        console.log('update entry: done');

        return true;
    },
    onNotFound: ({ translationPayload }) => {
        const entityType = translationPayload.metadata.entityType;
        const entityId = translationPayload.metadata.entity.id;

        // eslint-disable-next-line no-console
        console.log(`skip: can not find ${entityType} (${entityId})`);
    },
    onNotTranslatedResult: ({ updated, errorKind, errorText }) => {
        if (errorKind !== undefined) {
            // eslint-disable-next-line no-console
            console.warn(errorText || 'Unknown error');
        } else if (!updated) {
            // eslint-disable-next-line no-console
            console.log('Translation has no updates');
        }
    },
});
```

1. If everything goes fine - lets mark Phrase job as delivered:

   ```ts
   if (translationMerged) {
       // eslint-disable-next-line no-console
       console.log('update job status: start');

       await phraseClient.setJobStatus({
           projectUid: job.project.uid,
           jobUid: job.uid,
           jobStatus: 'DELIVERED',
       });

       // eslint-disable-next-line no-console
       console.log('update job status: done');
   }
   ```

## Register Webhook in Phrase TMS Dashboard

1. Open Phrase TMS dashdoard
2. Go to **Settings** and click **Webhooks**

   ![Phrase webhook settings location.](https://docs.uniform.app/images/integrations/translation-management/phrase/phrase-webhooks-settings-location.png)
3. Click **Add webhook**
4. Define webjook's name
5. Provide publicly avaiable url where Phrase is going to push job status notifications
6. Define webhook's secret token to validate income requests
7. Select **Job status changed** event

   ![Create Phrase webhook.](https://docs.uniform.app/images/integrations/translation-management/phrase/phrase-webhooks-create.png)
