Source: https://www.revvly.ai/docs/guides/support-knowledge-api

Guide

# Push your help articles to *Revvly Support*.

If your help content lives in your own system, send it to Revvly instead of having a site crawled. The support AI answers from what you send, and you say who may read each article, so nothing is guessed.

## [#](#when)When to use this

- **Push through the API**: Your articles are in a CMS, database or repository. The text is exactly what you send, and the audience is the one you state for each article.

- **Import a help centre**: You already publish on Intercom or Zendesk. Connect it in Support, Knowledge and Revvly reads it with your own token.

- **Crawl a site**: Your help pages are public web pages and you have no export. Audience is worked out from the page and held for your review.

i

A source filled through the API is not crawled and has no schedule. Revvly only changes it when you call the API (or an admin edits an article’s audience in the dashboard).

## [#](#key)Create a key that can add articles

Where: Org Settings > API keys > Create key > Can add knowledge articles. Admins and owners only. Revvly Support must be on for your workspace. The key is shown once.

The capability is called KNOWLEDGE_WRITE. It is separate from write access: a key that can add articles cannot change clients or subscriptions, and a write key cannot add articles. Keep it on your server only.

## [#](#upsert)Add or update articles

POST

https://api.revvly.ai/api/v1/graphql

application/json

Call upsertKnowledgeArticles with the key as the Bearer token. Send up to 50 articles per call. Sending an article again with the same externalId updates it, and an article whose text, address and audience have not changed costs nothing.

Mutation and variables

```
mutation Push($articles: [KnowledgeArticleInput!]!) {
  upsertKnowledgeArticles(source: "docs", syncId: "2026-10-11", articles: $articles) {
    results { externalId status unchanged note error }
  }
}

{
  "articles": [
    {
      "externalId": "billing-invoices",
      "title": "How to download an invoice",
      "url": "https://help.acme.com/billing/invoices",
      "markdown": "Open **Billing**, choose an invoice and select **Download PDF**.",
      "roles": ["founder", "admin"],
      "plans": ["growth", "scale"]
    },
    {
      "externalId": "getting-started",
      "title": "Getting started",
      "html": "<p>Create your workspace, then invite your team.</p>",
      "roles": []
    }
  ]
}
```

Response

```
{
  "data": {
    "upsertKnowledgeArticles": {
      "results": [
        { "externalId": "billing-invoices", "status": "INDEXED", "unchanged": false,
          "note": null, "error": null },
        { "externalId": "getting-started", "status": "INDEXED", "unchanged": false,
          "note": null, "error": null }
      ]
    }
  }
}
```

### Fields of each article

- **externalIdrequired**: Your own id for the article (max 200 characters). It is how a later push finds the same article.

- **titlerequired**: Shown with the answer as the source.

- **markdown or htmlone of**: The article body. Send exactly one; a row with both, or neither, is refused.

- **rolesrequired**: The support roles that may read it. Name each role by its internal key (shown on the role in the Roles panel of the Knowledge page, for example founder) or by its name, case-insensitive. The key never changes, so prefer it if a role may be renamed. An empty list means everyone, visitors included. It is required so an audience is never left to a guess, and an unknown role refuses the row.

- **plansoptional**: Narrows the article to some plans. Empty or omitted means every plan.

- **urloptional**: Where the article lives, as a full https address. It is the link in an answer.

### Sources and results

The source name (lowercase letters, digits, - and _; default default) groups your articles. The first push under a name creates the source, and you can have several. Each row succeeds or fails on its own: read results, which comes back in the order you sent, and look at error on each. Only a problem with the call itself fails the whole call.

i

If an admin has set an article’s audience in the dashboard, that wins over the one you send, and the row’s note says so.

## [#](#delete)Remove articles

deleteKnowledgeArticles removes up to 200 articles by your externalId. Ids that are not there are ignored, so it is safe to repeat.

```
mutation Remove($ids: [String!]!) {
  deleteKnowledgeArticles(source: "docs", externalIds: $ids) {
    removed
  }
}

{ "ids": ["billing-invoices"] }
```

## [#](#sync)Keep a source in step with yours

To mirror your content, send every article of a refresh with the same syncId (up to 128 characters), then close it with finishKnowledgeSync. Anything in the source that was not sent under that id is removed. This is the only way articles disappear without being named.

```
mutation Finish {
  finishKnowledgeSync(source: "docs", syncId: "2026-10-11") {
    removed
  }
}
```

A nightly job

```
// A nightly job on your server: push every article, then finish the refresh.
const SYNC_ID = new Date().toISOString();
const articles = await loadArticlesFromYourCms();

for (let i = 0; i < articles.length; i += 50) {
  const batch = articles.slice(i, i + 50).map((a) => ({
    externalId: a.id,
    title: a.title,
    markdown: a.markdown,
    url: a.publicUrl,
    roles: a.audienceRoles, // [] means everyone
  }));
  await revvly(
    `mutation($articles: [KnowledgeArticleInput!]!, $syncId: String) {
      upsertKnowledgeArticles(source: "docs", syncId: $syncId, articles: $articles) {
        results { externalId status error }
      }
    }`,
    { articles: batch, syncId: SYNC_ID },
  );
}

// Anything in the source that was not sent under SYNC_ID is removed.
await revvly(
  `mutation($syncId: String!) {
    finishKnowledgeSync(source: "docs", syncId: $syncId) { removed }
  }`,
  { syncId: SYNC_ID },
);
```

- **A typo removes nothing**: Finishing is refused when no article was sent with that syncId.

- **A refused row keeps its copy**: If an article was refused in this refresh, the copy Revvly already holds is kept, not deleted.

- **A running refresh is safe**: Finishing only removes what was last written before this refresh’s first article, so a refresh still running elsewhere is untouched.

## [#](#dashboard)See it in the dashboard

Where: Support > Knowledge. A pushed source shows as API, with its articles, their audience and the last result.

Pushed articles are searchable as soon as a row reports INDEXED. See also [signing users in to the chat](https://www.revvly.ai/docs/guides/support-tokens): the role value and attributes you send there decide which roles a person has, and the roles and plans you state here decide what their answers may draw on.

## Related

- [Build the knowledge base the AI answers from ›](https://www.revvly.ai/docs/guides/support-knowledge)
- [How the AI first line works ›](https://www.revvly.ai/docs/guides/support-ai)

