How to Integrate POEditor with Your Headless CMS
Send structured strings from your headless CMS to POEditor on publish, translate them by locale, and serve the right copy to every market without spreadsheet handoffs.
What is POEditor?
POEditor is a translation management platform for software, websites, apps, and content teams that need to localize strings across multiple languages. It supports projects, terms, translations, contributors, translation memory, API access, Git integrations, and file formats like JSON, XLIFF, PO, CSV, and Android XML. Teams use it when translators, developers, and editors need one place to track source strings, translation status, and locale-specific copy.
Why integrate POEditor with a headless CMS?
Localization gets messy when your source content lives in one system, translators work in another, and published pages need locale-ready copy at the same time. Without an integration, someone exports strings, uploads files to POEditor, waits for translations, downloads locale files, and asks a developer to merge them back into the site. That works for 20 strings. It breaks when you have 2,000 product descriptions, legal disclaimers, CTA labels, and help articles changing every week.
Architecture overview
A typical Sanity and POEditor integration starts when an editor publishes source-language content in Sanity Studio. A Sanity webhook fires on the publish mutation, or a Function runs directly on the content event. The handler receives the document ID, uses @sanity/client and GROQ to fetch the latest published fields from the Content Lake, then maps those fields into POEditor terms with stable keys such as article.introduction.title or product.sku123.description. The handler calls POEditor’s /v2/terms/add API with the project ID, API token, and JSON term payload. POEditor then exposes those terms to translators for each configured language. After translation, your return path can be a scheduled pull from POEditor’s export API, a CI job that downloads locale JSON files, or a second webhook-like process that writes approved translations back to Sanity. At runtime, the end user sees localized content from your frontend, which can read translated fields from Sanity or load locale files generated from POEditor.
Common use cases
Marketing site localization
Send page titles, hero copy, CTAs, and SEO descriptions from Sanity to POEditor whenever the English page is published.
Product catalog translation
Create POEditor terms for product names, descriptions, feature bullets, and disclaimers while keeping SKU and category references intact.
App and web string reuse
Use POEditor as the translation workspace for shared UI labels while Sanity structures longer editorial content for web, mobile, and in-product surfaces.
Locale release checks
Compare POEditor completion status with Sanity release plans so teams can hold a market launch until required languages are reviewed.
Step-by-step integration
- 1
Create a POEditor project and API token
In POEditor, create a project, add your source language, add target languages, and copy your API token from Account settings. Note the project ID from the project page or POEditor API response.
- 2
Install the packages for your sync code
Use @sanity/client to fetch content with GROQ. For a Node.js webhook handler, Node 18 or later includes fetch and FormData, so you can call POEditor’s REST API directly.
- 3
Model localized content in Sanity Studio
Add stable fields that can become translation keys, such as title, slug, excerpt, body, and seoTitle. For translated content, choose localized fields, locale-specific documents, or a separate translation document type before you start syncing production content.
- 4
Create the publish trigger
Add a Sanity webhook filtered to published documents, or use a Function for server-side sync on content mutations. Include the document ID and type in the payload so your handler can fetch the current published version.
- 5
Map Sanity fields to POEditor terms
Use GROQ to select only translatable fields, then convert each field into a POEditor term with a stable key, optional context, and reference. Call POEditor’s /v2/terms/add endpoint with token, project ID, and JSON data.
- 6
Test the full localization loop
Publish one test document, confirm the terms appear in POEditor, add a translation, export the target locale from POEditor, and render the translated content in your frontend. Test updates too, because changed source strings need a clear review policy.
Code example
import {createClient} from '@sanity/client'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false
})
export async function POST(req: Request) {
const {documentId} = await req.json()
const doc = await sanity.fetch(`
*[_id == $id][0]{
_type,
title,
excerpt,
"slug": slug.current,
seoTitle
}
`, {id: documentId.replace('drafts.', '')})
const terms = [
['title', doc.title],
['excerpt', doc.excerpt],
['seoTitle', doc.seoTitle]
].filter(([, value]) => typeof value === 'string' && value.length)
.map(([field, value]) => ({
term: `${doc._type}.${doc.slug}.${field}`,
definition: value,
context: `Sanity ${doc._type} field: ${field}`,
reference: documentId
}))
const body = new FormData()
body.set('api_token', process.env.POEDITOR_API_TOKEN!)
body.set('id', process.env.POEDITOR_PROJECT_ID!)
body.set('data', JSON.stringify(terms))
const res = await fetch('https://api.poeditor.com/v2/terms/add', {
method: 'POST',
body
})
if (!res.ok) throw new Error(await res.text())
return Response.json({synced: terms.length})
}How Sanity + POEditor works
Build your POEditor integration on Sanity
Use Sanity’s structured content foundation, real-time event system, and flexible APIs to send clean source strings to POEditor and publish localized experiences faster.
Start building free →CMS approaches to POEditor
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Translation source extraction | Often exports rendered pages or plugin-specific fields, so teams may clean up HTML before sending strings to POEditor. | Content Lake structures typed JSON, and GROQ selects exact fields for POEditor, including referenced content. |
| Sync timing | Translation exports often happen manually or through scheduled plugin jobs. | Webhooks or Functions can send source strings to POEditor on publish without polling. |
| Field-level control | Teams may translate full pages even when only one CTA or metadata field changed. | GROQ can project only title, excerpt, Portable Text blocks, SEO fields, and referenced labels in one query. |
| Editorial preview by locale | Preview depends on localization plugins and may not match the final frontend. | Sanity Studio, Presentation Tool, and structured locale fields can support click-to-edit previews for translated pages. |
| Translation return path | POEditor translations usually return through imports that can overwrite fields if mappings drift. | You can write translations into localized fields, locale documents, or release-specific drafts, but you’ll need to define the governance model. |
| Multi-channel delivery | Localized content is usually shaped around website pages first. | One structured back end can feed web, mobile, POEditor, AI agents, and other channels from the same content model. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Lokalise
Connect structured Sanity content to Lokalise for translation workflows, reviewer assignments, and locale delivery across digital products.
Sanity + Crowdin
Send source strings from Sanity to Crowdin so translators can work with context while developers keep content models in code.
Sanity + Phrase
Use Sanity with Phrase to coordinate product copy, marketing content, and app strings across multiple languages.