Skip to content
Dashboard

Edit an article body (find/replace)

POST/v2/help_center/articles/{id}/edit_body

Surgically edit an article’s body with a find/replace instruction, without re-uploading the whole HTML. Designed for AI agents doing high-precision, byte-stable edits (e.g. renaming a product across many articles).

Two-tier matching

Matching runs against the article’s stored body and is tried in two tiers:

  1. Text nodes only (mode: "text", preferred): oldText is matched only inside visible text — markup, tags, and attributes are never searched or altered, so the result is byte-identical everywhere outside the replaced spans. Matching is entity-aware, so oldText: "A & B" matches a stored A & B.
  2. Raw markup fallback (mode: "html"): only used when tier 1 finds nothing. oldText is matched as a literal substring over the raw body string (markup included), so you can swap an attribute value or tag. Because this can introduce markup, the full result is re-run through the XSS sanitizer before it is stored.

Do not include <img> tags in oldText — their stored src differs from the API representation and will not match.

Uniqueness rule

By default the edit requires oldText to match exactly once and returns 400 if it is ambiguous. To edit multiple occurrences, either set replaceAll: true or assert the exact count with expectedReplacements (which takes precedence over replaceAll).

Dry-run workflow

Set dryRun: true to preview: the response reports the match count, the matching mode, and previews (each changed span framed by surrounding context) without writing. Recommended flow for bulk edits: dry-run across the target articles first, confirm the counts and previews, then re-issue each call with dryRun: false.

Target

target: "draft" (default) edits the unpublished draft. target: "live" edits the published content directly — this records a history snapshot and purges the public edge cache, mirroring a publish.

Retry semantics

Calls are not idempotent — after a timeout, re-read the article (or dry-run) before retrying; with the default unique-match mode a duplicate retry fails safely unless newText still contains oldText.

Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer.

Path ParametersExpand Collapse
id: string

Article unique identifier

minLength1
maxLength16
Header ParametersExpand Collapse
"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"
One of the following:
"2026-01-01.nova"
"2025-12-12.clover"
Body ParametersJSONExpand Collapse
newText: string

Replacement text. Keep this to a fragment, not a whole document (max 10000 chars). May be an empty string to delete the matched fragment. When the match came from tier 2 (raw markup), the full resulting body is re-run through the XSS sanitizer, so any dangerous markup in newText is stripped. Images inserted via edit_body are not uploaded to Featurebase storage — use articles.update with formatter ‘raw’ for image changes.

maxLength10000
oldText: string

Exact fragment to find in the stored article body. Keep this to a fragment, not a whole document (max 10000 chars). Tier 1 matches only inside TEXT nodes (markup, tags, and attributes are never searched or altered) and is entity-aware, so oldText: "A & B" matches a stored A &amp; B. If tier 1 finds nothing, tier 2 falls back to a literal substring match over the raw body string (markup included). Do NOT include <img> tags — their stored src differs from the API representation and will not match.

minLength1
maxLength10000
dryRun: optional boolean

Preview the edit without writing. Returns the match count, matching mode, and previews (changed spans with surrounding context) so an agent can verify the edit before committing. Recommended workflow: dry-run across a set of articles first, confirm the counts and previews, then re-issue each call with dryRun: false.

expectedReplacements: optional number

Assert the exact number of matches. When set, the edit replaces all matches only if the count is exactly this value, otherwise it returns 400. Takes precedence over replaceAll. Use this to make bulk edits safe: dry-run first to learn the count, then commit with the expected number.

minimum1
locale: optional "bn" or "bs" or "pt-BR" or 39 more

Locale of the body to edit. Defaults to the help center’s default locale.

One of the following:
"bn"
"bs"
"pt-BR"
"bg"
"ca"
"hr"
"cs"
"da"
"nl"
"en"
"et"
"fi"
"fr"
"de"
"el"
"hi"
"hu"
"id"
"it"
"ja"
"ko"
"lv"
"lt"
"ms"
"mn"
"nb"
"pl"
"pt"
"ro"
"ru"
"sr"
"zh-CN"
"sk"
"sl"
"es"
"sw"
"sv"
"th"
"zh-TW"
"tr"
"uk"
"vi"
replaceAll: optional boolean

Replace every match. When omitted (or false) and expectedReplacements is not set, the edit requires exactly one match and returns 400 if oldText is ambiguous (found more than once). Provide more surrounding context in oldText to make a unique match, or set this to true.

target: optional "draft" or "live"

Which body to edit: the unpublished draft (default) or the published live content. Editing live records a history snapshot and purges the public edge cache, mirroring a publish.

One of the following:
"draft"
"live"
ReturnsExpand Collapse
EditArticleBodyResponse object { id, dryRun, locale, 6 more }
id: string

The edited article id

dryRun: boolean

Whether this was a preview (no write performed)

locale: string

Locale that was edited

matches: number

Number of matches found for oldText

mode: "text" or "html"

Which matching tier applied. text = text-node-only, byte-stable (markup untouched). html = raw-markup fallback (result re-sanitized). Tier 1 (text) is always attempted first.

One of the following:
"text"
"html"
object: "article.body_edit"

Object type identifier

previews: array of EditArticleBodyPreview { after, before }

Up to 20 before/after windows, one per replaced match, for verification. Preview strings are raw untrusted text (tier-2 previews show pre-sanitization content) — never render them as HTML.

after: string

The same span and window after the edit.

before: string

The changed span framed by up to 60 characters of surrounding context, before the edit.

replaced: number

Number of matches actually replaced (0 when dryRun is true)

target: "draft" or "live"

Which body was edited

One of the following:
"draft"
"live"

Edit an article body (find/replace)

curl https://do.featurebase.app/v2/help_center/articles/$ID/edit_body \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $FEATUREBASE_API_KEY" \
    -d '{
          "newText": "New Product Name",
          "oldText": "Old Product Name",
          "dryRun": true,
          "expectedReplacements": 3,
          "locale": "en",
          "target": "draft"
        }'
{
  "id": "1234567",
  "dryRun": false,
  "locale": "en",
  "matches": 1,
  "mode": "text",
  "object": "article.body_edit",
  "previews": [
    {
      "after": "…see the New Product Name dashboard for…",
      "before": "…see the Old Product Name dashboard for…"
    }
  ],
  "replaced": 1,
  "target": "draft"
}
Returns Examples
{
  "id": "1234567",
  "dryRun": false,
  "locale": "en",
  "matches": 1,
  "mode": "text",
  "object": "article.body_edit",
  "previews": [
    {
      "after": "…see the New Product Name dashboard for…",
      "before": "…see the Old Product Name dashboard for…"
    }
  ],
  "replaced": 1,
  "target": "draft"
}