Help centers
The customer-facing documentation portal: sections, multi-locale articles and their sync into the knowledge base.
Download the full API pública projection: openapi.json.
Publish your help center explains this resource in prose, with runnable examples.
GET /help-centers
List the tenant’s help centers
Every help center (portal) in the workspace, with its publish state and locales.
curl https://api.vitrinadev.com/api/v1/help-centers \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "a8a8a8a8-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"slug": "ayuda",
"name": "Centro de ayuda",
"brand": null,
"logo_url": null,
"primary_color": null,
"default_locale": "es",
"supported_locales": [
"es",
"en"
],
"custom_subdomain": null,
"is_published": true,
"settings": {},
"created_at": "2026-08-20T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers
Create a help center
A new help center starts unpublished — set is_published via PUT once its content is ready. slug is unique per tenant; a collision is a 409.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | yes | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
name | string | yes | mín. 1, máx. 160 |
brand | string | — | máx. 120 |
logo_url | string | — | uri, máx. 2048 |
primary_color | string | — | patrón ^#[0-9A-Fa-f]{6}$ |
default_locale | string | — | patrón ^[a-z]{2}(-[A-Z]{2})?$, por defecto "en" |
supported_locales | string[] | — | por defecto [] |
custom_subdomain | string | — | patrón ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Centro de ayuda",
"slug": "ayuda"
}'Example response (201)
{
"data": {
"id": "a8a8a8a8-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"slug": "ayuda",
"name": "Centro de ayuda",
"brand": null,
"logo_url": null,
"primary_color": null,
"default_locale": "es",
"supported_locales": [
"es",
"en"
],
"custom_subdomain": null,
"is_published": false,
"settings": {},
"created_at": "2026-08-20T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}
Delete a help center
Removes the portal and everything under it — sections, articles and translations.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}
Get one help center
The full portal row: branding, locales configuration, publish state.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "a8a8a8a8-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"slug": "ayuda",
"name": "Centro de ayuda",
"brand": null,
"logo_url": null,
"primary_color": null,
"default_locale": "es",
"supported_locales": [
"es",
"en"
],
"custom_subdomain": null,
"is_published": true,
"settings": {},
"created_at": "2026-08-20T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /help-centers/{id}
Update a help center
Partial update — at least one field is required. Nullable fields (brand, logo_url, primary_color, custom_subdomain) are cleared by sending null; omitting them leaves them untouched. is_published is what makes the portal reachable on the public reader.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | — | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
name | string | — | mín. 1, máx. 160 |
brand | string | null | — | máx. 120 |
logo_url | string | null | — | uri, máx. 2048 |
primary_color | string | null | — | patrón ^#[0-9A-Fa-f]{6}$ |
default_locale | string | — | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
supported_locales | string[] | — | |
custom_subdomain | string | null | — | patrón ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ |
is_published | boolean | — | |
settings | object | — |
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"is_published": true,
"primary_color": "#0EA5E9"
}'Example response (200)
{
"data": {
"id": "a8a8a8a8-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"slug": "ayuda",
"name": "Centro de ayuda",
"brand": null,
"logo_url": null,
"primary_color": "#0EA5E9",
"default_locale": "es",
"supported_locales": [
"es",
"en"
],
"custom_subdomain": null,
"is_published": true,
"settings": {},
"created_at": "2026-08-20T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/articles
List articles in a help center
Articles in this ONE portal — see GET /help-centers/articles/all for the tenant-wide list. Filter by section_id or status.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
section_id | query | uuid | no | |
status | query | draft \ | published \ | scheduled \ |
limit | query | integer | no | ≥ 1, ≤ 200 |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/articles \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "published",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles
Create an article
An empty translation.title is allowed on purpose: the editor opens the new article immediately and the title is typed inline. New articles are always created internal — publish them with the publish route.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | yes | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
section_id | string | null | — | |
position | integer | — | ≥ 0, ≤ 10000 |
translation | object | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"translation": {
"locale": "es",
"title": ""
}
}'Example response (201)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "internal",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "",
"body": ""
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/articles/{articleId}
Delete an article
Removes the article and every locale’s translation. A published article is un-published and its KB chunks are removed first, so the agent stops quoting it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/articles/{articleId}
Get one article with its translations
The full article row, every locale’s translation included.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "published",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /help-centers/{id}/articles/{articleId}
Update an article’s placement or status
Structure and lifecycle only — the prose lives in translations. Prefer the publish / archive / schedule routes over setting status here: they run the side effects (KB sync on publish, scheduling on schedule) that a bare status write does not.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | — | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
section_id | string | null | — | |
position | integer | — | ≥ 0, ≤ 10000 |
status | draft \ | published \ | scheduled \ |
visibility | public \ | internal | — |
scheduled_publish_at | string | null | — |
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"position": 1
}'Example response (200)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "published",
"position": 1,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/archive
Archive an article
Withdraws it from the reader without deleting it; can be published again later.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/archive \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "archived",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/articles/{articleId}/feedback
Summarise reader feedback for one article
Aggregate of the helpful / not-helpful votes left by readers on the public portal.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/feedback \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"helpful": 12,
"not_helpful": 1,
"total": 13
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/articles/{articleId}/lock
Release the editor lock
Only clears the lock if the caller holds it. Requires a Supabase user session for the same reason as taking it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/lock \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/lock
Take the editor lock on an article
Advisory lock so two editors do not overwrite each other. Requires a Supabase user session — an sk_* API key has no user behind it and gets 400, since a lock has to be held by someone. The response reports who holds it, so losing the race is a 200, not an error.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/lock \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"locked": true,
"locked_by": "11111111-0000-4000-8000-000000000001",
"locked_at": "2026-09-21T10:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/preview-link
Mint a signed preview token for an unpublished article
Lets a reviewer open a draft on the public reader without publishing it. The token is valid for 24 hours. locale may be sent in the body or as a ?locale= query parameter; neither is a 400.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
locale | string | yes | mín. 1 |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/preview-link \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"locale": "es"
}'Example response (200)
{
"data": {
"token": "ptok_9f1c2e7a8b3d4e5f",
"locale": "es",
"expires_in": 86400
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/publish
Publish an article
Makes it visible on the public reader AND syncs its translations into the knowledge base, so the AI agent starts answering from it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/publish \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "published",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/resync-kb
Force a fresh KB chunk + embed pass on a published article
Publishing already syncs to the KB, so this is the escape hatch for when that sync was lost — after a backfill, an embedding-model change, or an edit made while the background sync was failing. Only published articles can be synced; anything else is a 400.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/resync-kb \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"synced_locales": [
"es",
"en"
],
"chunks": 3
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/schedule
Schedule an article to publish later
Moves it to scheduled; a background worker publishes it at scheduled_publish_at, running the same KB sync as a manual publish.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
scheduled_publish_at | string | yes | mín. 1 |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/schedule \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_publish_at": "2026-09-25T13:00:00.000Z"
}'Example response (200)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "scheduled",
"position": 0,
"scheduled_publish_at": "2026-09-25T13:00:00.000Z",
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/articles/{articleId}/translations/{locale}
Delete an article translation
Removes this ONE locale’s translation. If the article is published and this was the last translation, the article-level KB entry for that locale is removed too.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes | |
locale | path | string | yes | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/translations/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
PUT /help-centers/{id}/articles/{articleId}/translations/{locale}
Create or replace an article translation
Upsert, and the route the editor autosaves through — which is why an empty title is accepted: it bounces 0-length drafts while the author is still typing. If the article is published, saving re-syncs the KB.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes | |
locale | path | string | yes | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
title | string | yes | máx. 240 |
body_markdown | string | yes | máx. 200000 |
excerpt | string | null | — | máx. 1000 |
meta_description | string | null | — | máx. 320 |
seo_title | string | null | — | máx. 240 |
seo_image | string | null | — | máx. 1024 |
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/translations/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Factory warranty",
"body": "Every new vehicle carries 3 years or 100,000 km of factory warranty."
}'Example response (200)
{
"data": {
"locale": "en",
"title": "Factory warranty",
"body": "Every new vehicle carries 3 years or 100,000 km of factory warranty."
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/{articleId}/translations/{locale}/generate
AI-translate one article into one locale
Synchronous single-cell counterpart to bulk-translate — it returns the finished translation rather than a job count, so it blocks for as long as the model takes. Overwrites any existing translation for that locale.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
articleId | path | uuid | yes | |
locale | path | string | yes | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/<id>/translations/<id>/generate \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"locale": "en",
"title": "Factory warranty",
"body": "Every new vehicle carries 3 years or 100,000 km of factory warranty.",
"generated": true
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/bulk-translate
Queue AI translation of every article into target locales
Asynchronous: enqueues one job per (article, locale) cell that needs work and returns immediately — the translations appear over the following minutes. min_status picks the cells: missing only fills gaps, outdated (the default) also re-translates cells whose source has changed since.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
target_locales | string[] | yes | |
min_status | missing \ | outdated | — |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/bulk-translate \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_locales": [
"en"
],
"min_status": "outdated"
}'Example response (202)
{
"enqueued": 4,
"target_locales": [
"en"
],
"articles": 4
}Answers: 202 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/articles/from-file
Create an article from an uploaded document
Extracts the text of a PDF / DOCX / XLSX / plain-text upload server-side and stores it as one article — no object-storage round trip. The slug and title come from the filename. Rejects with 400 when fewer than 20 characters of usable text come out, which is the usual outcome for a scanned image PDF.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/articles/from-file \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "file=@/path/to/file.png"Example response (201)
{
"data": {
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "internal",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "",
"body": ""
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/feedback
List reader feedback across the help center
Portal-wide feed of reader votes and comments — the per-article view is nested under the article.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/feedback \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"article_id": "b2b2b2b2-0000-4000-8000-000000000001",
"locale": "es",
"helpful": true,
"comment": null,
"created_at": "2026-09-20T15:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/locales
List the locales a help center publishes in
Every locale this portal publishes, with which one is is_default.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/locales \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"locale": "es",
"is_default": true
},
{
"locale": "en",
"is_default": false
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/locales
Add a locale to a help center
Answers 200, not 201 — an oddity of this route, kept for the clients already relying on it. Adding a locale does not translate anything; it opens the column that bulk-translate then fills.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
locale | string | yes | mín. 2, máx. 16 |
display_name | string | null | — | máx. 120 |
is_default | boolean | — | |
rtl | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/locales \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"locale": "en"
}'Example response (200)
{
"data": {
"locale": "en",
"is_default": false
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/locales/{locale}
Remove a locale from a help center
Drops the locale and every translation filed under it (sections and articles). The default locale cannot be removed — reassign it first.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
locale | path | string | yes | mín. 2, máx. 16 |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/locales/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/locales/{locale}/default
Make a locale the default
The default locale is what a reader gets with no locale preference, and the source language bulk-translate translates from.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
locale | path | string | yes | mín. 2, máx. 16 |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/locales/<id>/default \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"locale": "en",
"is_default": true
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/media
List uploaded media
Every media asset uploaded to this portal’s article editor.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/media \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "a8a8a8a8-4000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"url": "https://cdn.vitrinadev.com/help-centers/ayuda/garantia.png",
"content_type": "image/png",
"size_bytes": 48213,
"created_at": "2026-09-05T09:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/media
Upload a media asset (multipart/form-data)
Accepts images, video and documents for the article editor’s media blocks, up to 50 MB. Host anything larger externally and link to it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/media \
-H "Authorization: Bearer $VITRINA_KEY" \
-F "file=@/path/to/file.png"Example response (201)
{
"data": {
"id": "a8a8a8a8-4000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"url": "https://cdn.vitrinadev.com/help-centers/ayuda/garantia.png",
"content_type": "image/png",
"size_bytes": 48213,
"created_at": "2026-09-05T09:00:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/media/{mediaId}
Delete a media asset
Articles that still embed the asset are not rewritten — their image will 404.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
mediaId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/media/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/sections
List the section tree
Flat list; nesting is expressed by parent_id, ordering by position.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/sections \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "a9a9a9a9-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"slug": "garantia",
"parent_id": null,
"position": 0,
"created_at": "2026-08-20T12:10:00.000Z",
"updated_at": "2026-08-20T12:10:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/sections
Create a section
A section carries no text of its own — the title and description live in translations, so one is required at create time to name it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | yes | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
parent_id | string | null | — | |
position | integer | — | ≥ 0, ≤ 10000 |
translation | object | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/sections \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "garantia",
"translation": {
"locale": "es",
"title": "Garantía",
"description": "Todo sobre la garantía de fábrica"
}
}'Example response (201)
{
"data": {
"id": "a9a9a9a9-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"slug": "garantia",
"parent_id": null,
"position": 0,
"created_at": "2026-08-20T12:10:00.000Z",
"updated_at": "2026-08-20T12:10:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/sections/{sectionId}
Delete a section
Removes the section and its translations. Articles that were filed under it are not deleted — they lose their section (section_id becomes null).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
sectionId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/sections/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
PUT /help-centers/{id}/sections/{sectionId}
Update a section’s slug, parent or position
Structure only — title and description are per-locale, see the translation routes. A section cannot be reparented under itself.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
sectionId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
slug | string | — | patrón ^[a-z0-9](?:[a-z0-9-]{0,118}[a-z0-9])?$ |
parent_id | string | null | — | |
position | integer | — | ≥ 0, ≤ 10000 |
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/<id>/sections/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"position": 1
}'Example response (200)
{
"data": {
"id": "a9a9a9a9-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"slug": "garantia",
"parent_id": null,
"position": 1,
"created_at": "2026-08-20T12:10:00.000Z",
"updated_at": "2026-08-20T12:10:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /help-centers/{id}/sections/{sectionId}/translations/{locale}
Delete a section translation
Removes this ONE locale. The section itself, and its other locales, are untouched.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
sectionId | path | uuid | yes | |
locale | path | string | yes | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
curl -X DELETE https://api.vitrinadev.com/api/v1/help-centers/<id>/sections/<id>/translations/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
PUT /help-centers/{id}/sections/{sectionId}/translations/{locale}
Create or replace a section translation
Upsert — the same call creates the locale or overwrites it.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
sectionId | path | uuid | yes | |
locale | path | string | yes | patrón ^[a-z]{2}(-[A-Z]{2})?$ |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
title | string | yes | mín. 1, máx. 200 |
description | string | null | — | máx. 4000 |
curl -X PUT https://api.vitrinadev.com/api/v1/help-centers/<id>/sections/<id>/translations/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Garantía",
"description": "Todo sobre la garantía de fábrica"
}'Example response (200)
{
"data": {
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"locale": "es",
"title": "Garantía",
"description": "Todo sobre la garantía de fábrica"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /help-centers/{id}/sections/reorder
Reorder sections
Atomic: every section_id must belong to this help center, and one that does not fails the whole call rather than reordering the rest.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
order | object[] | yes |
curl -X POST https://api.vitrinadev.com/api/v1/help-centers/<id>/sections/reorder \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"section_ids": [
"a9a9a9a9-0000-4000-8000-000000000001"
]
}'Example response (200)
{
"data": [
{
"id": "a9a9a9a9-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"slug": "garantia",
"parent_id": null,
"position": 0,
"created_at": "2026-08-20T12:10:00.000Z",
"updated_at": "2026-08-20T12:10:00.000Z"
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/{id}/translation-coverage
Translation coverage matrix
Article × non-default-locale grid, each cell up_to_date | outdated | missing | draft, plus an overall percentage. This is the same computation bulk-translate uses to pick cells, so it doubles as a preview of what a bulk run would do.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/help-centers/<id>/translation-coverage \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"locales": [
"en"
],
"overall_pct": 75,
"articles": [
{
"article_id": "b2b2b2b2-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"en": "up_to_date"
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /help-centers/articles/all
List articles across every help center in the tenant
Tenant-wide article list, not scoped to one portal — drives the admin “all articles” view. Unlike the per-help-center list, this route does not validate status: an unknown value simply matches nothing.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
status | query | draft \ | published \ | scheduled \ |
curl https://api.vitrinadev.com/api/v1/help-centers/articles/all \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "b2b2b2b2-0000-4000-8000-000000000001",
"help_center_id": "a8a8a8a8-0000-4000-8000-000000000001",
"section_id": "a9a9a9a9-0000-4000-8000-000000000001",
"slug": "garantia-de-fabrica",
"status": "published",
"position": 0,
"scheduled_publish_at": null,
"translations": [
{
"locale": "es",
"title": "Garantía de fábrica",
"body": "Todos los vehículos nuevos incluyen 3 años o 100.000 km de garantía."
}
],
"created_at": "2026-08-21T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
Add-ons
The add-on catalog, the ones this workspace has ever held and the live usage of the metered ones — and the two self-serve writes to activate or cancel one.
Integrations · Meta Ads
The Meta Ads account connection: start it, resolve the account selection when there is more than one, disconnect it, and read the workspace's click-to-WhatsApp ads.