VitrinaAPI

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

EndpointWhat it does
POST /help-centers/{id}/articles/{articleId}/publishMakes it visible to the reader and syncs its translations into the knowledge base
POST /help-centers/{id}/articles/{articleId}/archiveWithdraws it without deleting it, and it can be published again
POST /help-centers/{id}/articles/{articleId}/scheduleLeaves 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.

On this page