---
title: Syncing
description: Read new and changed messages from a source, from the backoffice or a deployment pipeline, and tell your application when anything changes.
seo:
  image: /og/syncing.png
---

## Syncing a source

Umbraco reads a source when someone presses **Sync now** in **Settings > App translations**. **History** next to it lists every sync, with what it added, changed and removed, or why it failed. Umbraco can also sync [on a schedule](#syncing-on-a-schedule), and a deployment can sync through [the endpoint below](#syncing-from-a-deployment).

## Syncing on a schedule

To pick up new messages without anyone pressing a button, choose how often under **Sync automatically** in the source's settings: every hour, every 6 or 12 hours, every day or every week. Umbraco then syncs the source once its last sync is that old, whether that sync was started from the backoffice, a deployment or the schedule. The source shows when it syncs next.

- A failed sync counts too, so a source that is down is tried again at the next interval rather than every few minutes. **History** says why it failed.
- A sync that finds nothing new changes nothing and is not announced, so a short interval costs your application nothing.
- In a load-balanced setup, only the Single or SchedulingPublisher server runs the schedule.

A deployment that syncs as it ships new translation files is still the quickest way to get them in. The schedule catches the files that change some other way.

## What a sync changes

A sync compares the files with what Umbraco has and applies the difference. Custom text is never lost.

- **A new key** arrives with the application's text.
- **Changed default text** replaces the old default. Custom text written for that message stays, marked **Default changed** so an editor can check it still fits.
- **A key the files no longer have** is deleted if nobody wrote custom text for it. If somebody did, the message stays as **No longer used**, with its custom text, but the delivery endpoints stop serving it. If the key comes back, so does the text.
- **Text written for a language the source does not supply** is left alone. See [Languages your application does not ship](/editing#languages-your-application-does-not-ship).

A sync that reads exactly what Umbraco already has changes nothing, and is not announced.

## Syncing from a deployment

A deployment can sync a source by its identifier, without a backoffice login. The endpoint is off until you give your pipeline a way in: a secret, or the OIDC token its platform already issues. You can set up both and move pipelines over one at a time.

Have the pipeline call the endpoint once the new translation files are live. Replace `website` with the source identifier from the source's **Advanced** settings. The request waits for the sync to finish and returns how many messages it added, changed and removed.

The [API reference](/endpoints/sync) lists what it returns, and every status code.

```mermaid
---
config:
  sequence:
    mirrorActors: false
---
sequenceDiagram
    participant P as Pipeline
    participant F as Translation files
    participant U as Umbraco
    participant A as Your application
    P->>F: Deploy the new files
    P->>U: POST .../sources/website/sync<br/>with a secret or an OIDC token
    U->>F: Read every language
    Note over U: Apply the difference,<br/>keeping custom text
    U-->>P: Added, changed and removed counts
    opt Something changed
        U->>A: App translations updated webhook
    end
```

### With a secret

Configure a long random value. Anyone who has it can start a sync.

```sh
AppTranslations__DeploymentSync__ApiKey=replace-with-a-long-random-value
```

Give your deployment pipeline the same secret, and send it as a bearer token.

```sh
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer $UMBRACO_TRANSLATIONS_SYNC_KEY" \
  "$UMBRACO_URL/umbraco/app-translations/api/v1/sources/website/sync"
```

:::caution
Use HTTPS, and keep the secret in the secret stores on both sides.
:::

### With an OIDC token

GitHub Actions, GitLab CI and the big cloud platforms can give a job a short-lived token that proves which repository or workload it came from. Send that token instead of a secret, and there is nothing to store or rotate.

App Translations doesn't check the token itself. Instead, you register an ASP.NET Core authorization policy under the name `TranslationPolicies.DeploymentSync`, and any request that passes the policy can start a sync. [API OIDC](https://api-oidc.thebuilder.dk/), a separate package, checks the tokens. To set it up for GitHub Actions:

1. Install API OIDC, version 0.2.3 or later, and trust your repository's tokens, as its [Issuers](https://api-oidc.thebuilder.dk/issuers) page describes:

   ```json appsettings.json
   {
     "ApiOidc": {
       "Issuers": [
         { "Preset": "GitHubActions", "Owner": "acme", "Repositories": ["acme/site"] }
       ]
     }
   }
   ```

2. Register the policy in `Program.cs`. API OIDC's `RequireOidcToken` lets in a token it accepted from the issuers you name:

   ```csharp Program.cs
   using TheBuilder.ApiOidc.Authentication;
   using TheBuilder.AppTranslations.Authorization;

   builder.Services.AddAuthorization(options => options.AddPolicy(
       TranslationPolicies.DeploymentSync, policy => policy.RequireOidcToken("GitHubActions")));
   ```

   `GitHubActions` is the issuer's `Name` in API OIDC, which is the preset's name unless you set another. Checking it matters when API OIDC also trusts your frontends: their tokens are valid too, and get a `403` here.

3. In the workflow, get a token and send it in the `X-Oidc-Token` header:

   ```yaml .github/workflows/deploy.yml
   jobs:
     deploy:
       runs-on: ubuntu-latest
       permissions:
         id-token: write
       env:
         UMBRACO_URL: ${{ vars.UMBRACO_URL }}
       steps:
         # Deploy the new translation files first.
         - name: Sync translations
           run: |
             token=$(curl --silent --fail \
               --header "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
               "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq --raw-output .value)
             curl --fail-with-body \
               --request POST \
               --header "X-Oidc-Token: $token" \
               "$UMBRACO_URL/umbraco/app-translations/api/v1/sources/website/sync"
   ```

   The token is made for GitHub's default audience, which is what the `GitHubActions` preset expects. If you gave the issuer your own `Audience`, add it to the token request: `"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://cms.example.com"`.

#### Writing your own policy

The policy can check anything ASP.NET Core can. Two rules keep it safe:

- **It must read a token the caller sends.** Name the authentication scheme that reads it: `RequireOidcToken` does that for you, and with `AddAuthenticationSchemes` you name it yourself. A policy with no scheme, or with a cookie scheme, would let in whoever is signed in to the browser, so a web page an editor opens could start a sync. The site won't start with a policy like that.
- **It alone decides who gets in.** If it lets anonymous requests through, anyone can start a sync. For example, don't build it from API OIDC's `TheBuilder.Oidc.DeliveryApiAccess` policy: while the Delivery API is public, that lets everyone in.

## Telling your application something changed

To drop a cached dictionary the moment it changes, add a webhook in **Settings > Webhooks** for the **App translations updated** event (under **Other**). Umbraco handles the URL, headers, retries and the request log as it does for any webhook.

The event fires once for every change that reaches the delivery endpoints.

| `change` | When |
| --- | --- |
| `Synchronized` | A sync added, changed or removed at least one message. |
| `OverrideSaved` | An editor saved custom text. |
| `OverrideRemoved` | An editor reverted custom text to the default. |
| `SourceDeleted` | Someone deleted a source, and everything it provided. |

```json
{
  "sourceId": "6f2f5d3e-1f4a-4a1e-9a5b-2c7d8e9f0a1b",
  "sourceAlias": "website",
  "change": "OverrideSaved",
  "occurredAt": "2026-08-26T09:30:00+00:00",
  "message": { "namespace": "website", "key": "cart.title", "locale": "da-DK" },
  "synchronization": null
}
```

For a saved or reverted message, `message` names it. Its locale and namespace tell you exactly which dictionary to purge. For a sync, `synchronization` holds the revision and the added, changed and removed counts. Umbraco does not announce a sync that found nothing new.

Umbraco sends webhooks only when `Umbraco:CMS:Webhook:Enabled` is on, which it is by default. In a load-balanced setup, Umbraco sends webhooks only from the Single or SchedulingPublisher server.
