Publish your help center
Stand up the self-service portal and sync it into the agent.
POST /help-centers stands up the self-service portal your customers read on their own, with FAQs, policies and how-to guides. It isn't the same library the agent quotes from, but the two are wired together. Publishing an article syncs it into the knowledge base, and the agent starts quoting it with the same text the reader sees.
Reading needs help_centers:read; writing, help_centers:write. A workspace can run more than one help center.
Creating a center
curl -X POST https://api.vitrinadev.com/api/v1/help-centers \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Help center", "slug": "help" }'It's born unpublished (is_published: false). Publish it with PUT once it has content:
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001 \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_published": true, "primary_color": "#0EA5E9" }'slug is unique per workspace and decides the reader's URL. default_locale and supported_locales decide which languages the portal exists in; that part is set up under Locales.
Sections
A section is a folder in the navigation tree, and it carries no text of its own. Its title and description are a translation, so creating one requires a translation from the start:
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/sections \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "policies",
"translation": { "locale": "en", "title": "Policies", "description": "Refunds, exchanges and warranties" }
}'parent_id nests a section under another, and position decides the order. POST /help-centers/{id}/sections/reorder reorders in one call, and it's atomic: if one section_id doesn't belong to this center, none of them move.
Articles
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/articles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "translation": { "locale": "en", "title": "" } }'{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": null,
"slug": "new-article",
"status": "internal",
"position": 0,
"scheduled_publish_at": null,
"translations": [{ "locale": "en", "title": "", "body": "" }],
"created_at": "2026-09-22T12:00:00.000Z",
"updated_at": "2026-09-22T12:00:00.000Z"
}
}The article can be born with an empty title. It's born internal, and nobody outside sees it until you publish it. POST /help-centers/{id}/articles/from-file creates one straight from an uploaded PDF, DOCX or XLSX, extracting the text server-side.
The publish cycle
| Endpoint | What it does |
|---|---|
POST /help-centers/{id}/articles/{articleId}/publish | Makes it visible to the reader and syncs its translations into the knowledge base |
POST /help-centers/{id}/articles/{articleId}/archive | Withdraws it without deleting it, and it can be published again |
POST /help-centers/{id}/articles/{articleId}/schedule | Leaves it scheduled; it publishes itself at scheduled_publish_at, with the same sync |
PUT /help-centers/{id}/articles/{articleId} only touches structure and lifecycle, meaning section and position. The text is edited through the translation routes.
Translations
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/articles/b2b2b2b2-0000-4000-8000-000000000001/translations/$LOCALE \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Política de reembolsos", "body": "Los reembolsos se emiten dentro de 10 días hábiles." }'This is the route the editor autosaves through, and it accepts an empty title while the author is still typing. If the article is already published, saving re-syncs that translation into the knowledge base immediately.
POST /help-centers/{id}/articles/{articleId}/translations/{locale}/generate AI-translates one article. It's synchronous and overwrites whatever existed for that locale.
POST /help-centers/{id}/articles/bulk-translate does the same for every article in the center, queued. min_status picks which cells to touch: missing only fills gaps, and outdated, the default, also re-translates cells whose source has changed.
curl "https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/translation-coverage" \
-H "Authorization: Bearer $VITRINA_KEY"Returns the article × locale matrix (up_to_date, outdated, missing, draft) from the same computation that decides what bulk-translate touches. Use it to preview what a run would do before launching it.
Reviewing before publishing
POST /help-centers/{id}/articles/{articleId}/preview-link mints a signed token, valid 24 hours, to read an article without publishing it. With that link somebody reviews the draft on the real portal.
POST /help-centers/{id}/articles/{articleId}/lock takes the editor lock so two people don't overwrite each other. It requires a user session, so an API key with nobody behind it can't take one. It reports who holds the lock even when you lose the race. DELETE /help-centers/{id}/articles/{articleId}/lock releases it.
Reader feedback
curl "https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/articles/b2b2b2b2-0000-4000-8000-000000000001/feedback" \
-H "Authorization: Bearer $VITRINA_KEY"{ "data": { "helpful": 12, "not_helpful": 1, "total": 13 } }GET /help-centers/{id}/feedback returns the same aggregate vote across the whole center.
Locales
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/a8a8a8a8-0000-4000-8000-000000000001/locales \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "locale": "es" }'Adding a locale translates nothing: it opens the column bulk-translate fills afterwards. POST /help-centers/{id}/locales/{locale}/default changes which one a reader with no stated preference gets, and which one bulk-translate translates from. The default locale can't be removed with DELETE /help-centers/{id}/locales/{locale}; change which one is the default first.
Media
POST /help-centers/{id}/media uploads images, video or documents for the editor's media blocks, multipart and up to 50 MB. DELETE /help-centers/{id}/media/{mediaId} deletes the file. An article that still embeds it isn't rewritten, so its image starts answering 404.
Deleting
DELETE /help-centers/{id} takes its sections, articles and translations with it. Deleting a section or an article is final, and there's no trash.