Releases and OTA
An export is a file you commit. A release is a snapshot your app downloads. Releases are how you change a translation without shipping a new build.
Creating a release
Section titled “Creating a release”From the project’s Releases tab, or the GraphQL createRelease mutation. A release takes the approved translations of the project as they stand right now and freezes them.
| Field | Meaning |
|---|---|
version | Assigned automatically, incrementing per project |
name, description | Yours, to say what changed |
splitByFiles | Whether the bundle is grouped by file — see below |
languages | Which languages ended up in the bundle |
segmentCount | How many keys the fullest language carries |
Only approved translations go in. A language nobody has finished appears with the keys that are done and nothing else, so a release is never blocked by an incomplete language — but it is worth checking progress before you cut one.
Bundle shape
Section titled “Bundle shape”By default a bundle is a flat map of key to value, one map per language, with the source language carrying the source texts:
{ "en": { "home.title": "Hello" }, "de": { "home.title": "Hallo" }}With splitByFiles, the keys are grouped by the file they came from:
{ "en": { "5f1c…": { "home.title": "Hello" } }, "de": { "5f1c…": { "home.title": "Hallo" } }}Use the flat shape when your app loads one dictionary per language, and the split shape when it loads a namespace at a time.
A segment with plural forms carries its whole form map as the value, so plurals survive a release intact — which is the reason to prefer a bundle over a flat file export for them.
Duplicate keys
Section titled “Duplicate keys”In a flat bundle the same key in two files can only appear once. When that happens the release records it: the key, the files it came from, and which file the value was actually taken from — the last one wins. The list comes back with the release and is shown in the UI.
That report is worth reading the first time you release a project split across many files. Either rename the colliding keys, or switch the release to splitByFiles so the collision disappears.
OTA distribution
Section titled “OTA distribution”Every project has one distribution: a permanent access key that serves its releases over HTTP with no authentication.
Find it under Releases → Distribution, or with the GraphQL releaseDistribution query. If the key leaks, regenerate it — the old key stops working immediately, and every client will need the new one.
The endpoints
Section titled “The endpoints”GET https://api.ownlate.com/public/v1/ota/{accessKey}/manifestGET https://api.ownlate.com/public/v1/ota/{accessKey}/bundlesGET https://api.ownlate.com/public/v1/ota/{accessKey}/bundles/{language}The manifest is small and cheap to poll:
{ "version": 7, "languages": ["en", "de", "fr"], "publishedAt": "2026-08-20T09:31:00Z" }The bundle endpoints return the translations, either for all languages or for one:
{ "version": 7, "language": "de", "splitByFiles": false, "publishedAt": "2026-08-20T09:31:00Z", "translations": { "home.title": "Hallo" }}All three serve the latest release by default. Add ?version=5 to pin an older one — useful when an old app build expects keys the current release no longer has.
Caching
Section titled “Caching”Responses carry an ETag derived from the version and publication time. Send If-None-Match and an unchanged bundle answers 304 Not Modified with no body. Bundles are compressed when the client says it accepts it, and the compressed form is cached too.
A sensible client asks for the manifest on start-up, compares the version to what it has stored, and downloads the bundle only when it differs.