> ## Documentation Index
> Fetch the complete documentation index at: https://help.semji.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync drafts to your CMS

> Poll Semji for drafts ready to publish, push them to your CMS, then mark them as published in Semji, with a push-based webhook alternative.

This guide shows you how to build a one-way sync from Semji to your CMS (WordPress, Contentful, Webflow, a custom backend, etc.). The flow runs on a schedule — for example every 10 minutes — and uses four endpoints:

1. **List drafts** with a custom workflow status (e.g. *Ready to publish*).
2. **Fetch the full content** (title, sanitized HTML, meta description) for each draft.
3. **Push to the CMS** and immediately transition the draft to a second custom status (e.g. *Sent to CMS*).
4. **Mark each content as published** in Semji once the CMS confirms the article is live.

<Note>
  This main flow is **pull-based**: your worker polls Semji because no webhook fires on custom workflow status changes. Semji *does* fire a webhook when a content is **marked as published** — if your editors manage publication from Semji, see the [push-based alternative](#push-based-alternative-with-webhooks) below.
</Note>

## Why two custom statuses?

Between *"the editor approved the draft"* and *"the article is live on the public site"*, the article can sit in the CMS for hours or days — review, scheduled publication, legal moderation… You need a status that means **"already handed over to the CMS, don't push it again"** while you wait.

| Stage | Semji status | Set by | Purpose |
| - | - | - | - |
| Editorially approved, ready to push | `Ready to publish` (custom) | Editor, in the app | Signals the worker to pick it up |
| Pushed to the CMS, awaiting publication | `Sent to CMS` (custom) | Worker, right after the CMS accepts it | Prevents re-pushing on the next poll |
| Live on the CMS | `published` (system) | Worker, via `POST /contents/{contentId}/publish` | Records the final URL and `publishedAt` |

Skipping the intermediate status causes one of two bugs:

* **Marking as published right after the push** — `publishedAt` is wrong (the CMS may schedule for next week) and the URL may still change (slug under review).
* **Leaving the draft in *Ready to publish*** — the next poll re-pushes the same article, creating duplicates in the CMS.

## Prerequisites

* An API key with access to the workspace. See [Authentication](/api-reference/authentication).
* The **workspace ID** you want to sync from. Get it from [`GET /v1/workspaces`](/api-reference/workspaces/list-workspaces).
* Two custom workflow statuses: *Ready to publish* and *Sent to CMS*.

## 1. Create the two custom statuses

Workflow statuses are configured per workspace in the Semji app under **Settings > Workflow**. Create two non-system statuses:

* *Ready to publish* — editors move drafts here when they're approved.
* *Sent to CMS* — the worker moves drafts here right after the CMS accepts them.

List every status (with its ID) via the API:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const API = "https://api.semji.com/v1";
  const HEADERS = { Authorization: `Bearer ${process.env.SEMJI_API_KEY}` };
  const WORKSPACE_ID = process.env.WORKSPACE_ID;

  const response = await fetch(
    `${API}/workspaces/${WORKSPACE_ID}/content-statuses`,
    { headers: HEADERS },
  );
  const { data: statuses } = await response.json();

  const byLabel = new Map(
    statuses.map((status) => [status.label.toLowerCase(), status.id]),
  );
  const READY_ID = byLabel.get("ready to publish");
  const SENT_ID = byLabel.get("sent to cms");
  ```

  ```python Python theme={null}
  import os, requests

  WORKSPACE_ID = os.environ["WORKSPACE_ID"]
  headers = {"Authorization": f"Bearer {os.environ['SEMJI_API_KEY']}"}

  statuses = requests.get(
      f"https://api.semji.com/v1/workspaces/{WORKSPACE_ID}/content-statuses",
      headers=headers,
  ).json()["data"]

  by_label = {s["label"].lower(): s["id"] for s in statuses}
  READY_ID = by_label["ready to publish"]
  SENT_ID  = by_label["sent to cms"]
  ```

  ```bash cURL theme={null}
  curl https://api.semji.com/v1/workspaces/$WORKSPACE_ID/content-statuses \
    -H "Authorization: Bearer $SEMJI_API_KEY"
  ```
</CodeGroup>

A successful response looks like this:

```json title="200 OK" theme={null}
{
  "data": [
    { "id": "b3d51e92c804", "label": "to do",            "isReadOnly": true,  "position": 1, "color": "#9ca3af" },
    { "id": "29f7c6a1d05e", "label": "in progress",      "isReadOnly": false, "position": 2, "color": "#3498db" },
    { "id": "8a1c4e6f02b9", "label": "ready to publish", "isReadOnly": false, "position": 3, "color": "#f59e0b" },
    { "id": "d6e90b37a512", "label": "sent to cms",      "isReadOnly": false, "position": 4, "color": "#8b5cf6" },
    { "id": "41f8a2c5e7d3", "label": "published",        "isReadOnly": true,  "position": 5, "color": "#10b981" }
  ]
}
```

System statuses (`to do`, `published`) have `isReadOnly: true` and can't be renamed or deleted. Cache both custom IDs — you'll use them in the next steps.

<Tip>
  See [List content statuses](/api-reference/workspaces/list-content-statuses) for the full schema.
</Tip>

## 2. Poll for drafts ready to publish

Call [`GET /v1/workspaces/{workspaceId}/contents`](/api-reference/contents/list-contents-in-a-workspace) filtered on the *Ready to publish* status:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const query = new URLSearchParams({
    "contentStatusId[]": READY_ID,
    sort: "-updatedAt",
    limit: "100",
  });
  const { data: drafts, pagination } = await (
    await fetch(`${API}/workspaces/${WORKSPACE_ID}/contents?${query}`, {
      headers: HEADERS,
    })
  ).json();
  ```

  ```python Python theme={null}
  response = requests.get(
      f"https://api.semji.com/v1/workspaces/{WORKSPACE_ID}/contents",
      headers=headers,
      params={"contentStatusId[]": READY_ID, "sort": "-updatedAt", "limit": 100},
  ).json()
  drafts, pagination = response["data"], response["pagination"]
  ```

  ```bash cURL theme={null}
  curl "https://api.semji.com/v1/workspaces/$WORKSPACE_ID/contents?contentStatusId[]=$READY_ID&sort=-updatedAt&limit=100" \
    -H "Authorization: Bearer $SEMJI_API_KEY"
  ```
</CodeGroup>

The collection endpoint returns lightweight content records (no HTML body). You fetch the body in the next step.

The response is **paginated**: `limit` caps at 100 (default 25), and the envelope carries a `pagination` object (`total`, `page`, `limit`, `hasMore`). If `hasMore` is `true`, request the next page with `?page=2`, and so on — the full worker below does this. Don't assume one page is enough: a backlog (worker downtime, a bulk editorial approval) can easily exceed 100 drafts.

<Note>
  Each API key is limited to **1,000 requests per hour** with a burst of 20/s. Polling every 10 minutes (= 6/hour) leaves plenty of headroom even for hundreds of drafts per poll. See [Rate limits](/api-reference/rate-limits).
</Note>

## 3. Push to the CMS and transition to *Sent to CMS*

For each draft, fetch the full body with [`GET /v1/contents/{contentId}`](/api-reference/contents/get-content-details), push it to your CMS, then **immediately** transition the draft to *Sent to CMS* with [`PUT /v1/contents/{contentId}`](/api-reference/contents/update-a-content).

<CodeGroup>
  ```typescript TypeScript theme={null}
  const content = await (
    await fetch(`${API}/contents/${draft.id}`, { headers: HEADERS })
  ).json();

  // Push title, htmlSanitized, metaDescription to the CMS (throws on failure)
  const cmsRecordId = await pushToCms(content);

  // Mark as handed over so the next poll skips it
  const update = await fetch(`${API}/contents/${content.id}`, {
    method: "PUT",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ contentStatusId: SENT_ID, version: content.version }),
  });
  if (!update.ok) throw new Error(`PUT failed: ${update.status}`);
  ```

  ```python Python theme={null}
  content = requests.get(
      f"https://api.semji.com/v1/contents/{draft['id']}",
      headers=headers,
  ).json()

  # Push title, htmlSanitized, metaDescription to the CMS (raises on failure)
  cms_record_id = push_to_cms(content)

  # Mark as handed over so the next poll skips it
  requests.put(
      f"https://api.semji.com/v1/contents/{content['id']}",
      headers=headers,
      json={"contentStatusId": SENT_ID, "version": content["version"]},
  ).raise_for_status()
  ```

  ```bash cURL theme={null}
  # Fetch the body
  CONTENT=$(curl -s https://api.semji.com/v1/contents/$CONTENT_ID \
    -H "Authorization: Bearer $SEMJI_API_KEY")
  VERSION=$(echo "$CONTENT" | jq -r .version)

  # (… push title, htmlSanitized, metaDescription to your CMS …)

  # Transition to "Sent to CMS"
  curl -X PUT https://api.semji.com/v1/contents/$CONTENT_ID \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"contentStatusId\": \"$SENT_ID\", \"version\": $VERSION}"
  ```
</CodeGroup>

A few important details:

* **Push `htmlSanitized`, not `html`.** The `html` field is the body as authored in the Semji editor and may contain editor-only markers (comments, fact-check annotations). `htmlSanitized` is the same body with all annotations stripped — that's the one that is safe to publish externally.
* **`version` is required on every update**, even when you only change the status. Reuse the `version` from your `GET`. If anything modified the content between your `GET` and your `PUT` (an editor saving, for instance), the API returns `409 Conflict`. A 409 doesn't mean your transition is invalid — refetch to get the fresh `version` and retry the same update.
* **Transition only on CMS success.** If the CMS call fails, leave the draft in *Ready to publish* and the next poll will retry it.
* **Persist the CMS record ID** (e.g. WordPress post ID) on your side, keyed by Semji `content.id`. You'll need it in step 4 to know when the article goes live.

```json title="GET /v1/contents/{contentId} (excerpt)" theme={null}
{
  "id": "7c4a1f08b29d",
  "title": "How to choose a CRM in 2026",
  "html": "<h1>How to choose a CRM in 2026</h1><p>…</p><!-- semji editor annotations -->",
  "htmlSanitized": "<h1>How to choose a CRM in 2026</h1><p>…</p>",
  "metaDescription": "A practical guide to picking the right CRM…",
  "page": { "id": "5e8d203c7f1a", "url": "https://example.com/blog/choose-a-crm" },
  "contentStatus": { "id": "8a1c4e6f02b9", "label": "ready to publish" },
  "version": 7
}
```

## 4. Mark as published when the CMS goes live

The CMS publication can happen seconds or days after the push. You need a separate trigger that fires when the article actually goes live. Pick the option that fits your CMS:

* **CMS webhook** *(recommended when available)* — WordPress, Contentful, Webflow, and most modern CMSes can fire a webhook on publish. Wire it to a small endpoint that looks up the Semji `content.id` from the CMS record ID and calls Semji.
* **Second poll loop** — every 10 min, list all your *Sent to CMS* drafts, check each one against the CMS API to see if it's now public, and mark the ones that are.

In both cases, the call is the same: [`POST /v1/contents/{contentId}/publish`](/api-reference/contents/mark-a-content-as-published).

<CodeGroup>
  ```typescript TypeScript theme={null}
  const publish = await fetch(`${API}/contents/${contentId}/publish`, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ url: cmsPublicUrl }),
  });
  if (!publish.ok) throw new Error(`publish failed: ${publish.status}`);
  ```

  ```python Python theme={null}
  requests.post(
      f"https://api.semji.com/v1/contents/{content_id}/publish",
      headers=headers,
      json={"url": cms_public_url},
  ).raise_for_status()
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.semji.com/v1/contents/$CONTENT_ID/publish \
    -H "Authorization: Bearer $SEMJI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://www.example.com/blog/choose-a-crm"}'
  ```
</CodeGroup>

The request body is optional:

* `url` *(optional)* — the public URL where the article is now live. If omitted, Semji reuses the URL of the associated page. Pass it explicitly when you're publishing a **new** page or when the CMS slug differs from the original page URL.
* `publishedAt` *(optional, ISO 8601)* — the publication date recorded in Semji, the same one an author picks in the editor. If omitted, the current server time is used. Pass it when the publication is detected with a delay — e.g. your poll loop discovers an article that went live earlier — so performance tracking starts from the real publication date.

  The moment this call is processed is recorded separately as `markedAsPublishedAt`. Semji sets it, it cannot be sent in the body, and it is the field to filter on when you need to detect publications.

<Warning>
  If you omit `url` **and** the associated page has no URL, the call fails with `422 unprocessable_entity`. This is the typical case for contents created in Semji without a `pageId` — their auto-created page starts with an empty URL. When in doubt, pass `url` explicitly.
</Warning>

Once this call returns, the draft transitions from *Sent to CMS* to the system *published* status.

## Putting it all together

A minimal worker that polls every 10 minutes for both transitions:

<CodeGroup>
  ```typescript sync-drafts.ts expandable theme={null}
  const API = "https://api.semji.com/v1";
  const HEADERS = {
    Authorization: `Bearer ${process.env.SEMJI_API_KEY}`,
    "Content-Type": "application/json",
  };
  const WORKSPACE_ID = process.env.WORKSPACE_ID;

  // Implement these against your CMS and storage:
  declare function pushToCms(content: { htmlSanitized: string | null }): Promise<string>;
  declare function rememberCmsRecord(contentId: string, cmsRecordId: string): Promise<void>;
  declare function lookupCmsRecord(contentId: string): Promise<string>;
  declare function fetchCmsState(cmsRecordId: string): Promise<{ isPublic: boolean; publicUrl: string }>;

  class ApiError extends Error {
    constructor(readonly status: number, message: string) {
      super(message);
    }
  }

  async function api(path: string, init: RequestInit = {}) {
    const response = await fetch(`${API}${path}`, { ...init, headers: HEADERS });
    if (!response.ok) {
      throw new ApiError(response.status, `${init.method ?? "GET"} ${path} → ${response.status}`);
    }
    return response.json();
  }

  // --- status lookup -----------------------------------------------------------

  async function loadStatusIds() {
    const { data: statuses } = await api(`/workspaces/${WORKSPACE_ID}/content-statuses`);
    const byLabel = new Map(statuses.map((s) => [s.label.toLowerCase(), s.id]));
    return { readyId: byLabel.get("ready to publish"), sentId: byLabel.get("sent to cms") };
  }

  // --- helpers -----------------------------------------------------------------

  async function listByStatus(statusId: string) {
    const drafts = [];
    for (let page = 1; ; page++) {
      const query = new URLSearchParams({
        "contentStatusId[]": statusId,
        sort: "-updatedAt",
        limit: "100",
        page: String(page),
      });
      const { data, pagination } = await api(`/workspaces/${WORKSPACE_ID}/contents?${query}`);
      drafts.push(...data);
      if (!pagination.hasMore) return drafts;
    }
  }

  async function transitionTo(contentId: string, statusId: string) {
    for (let attempt = 1; attempt <= 3; attempt++) {
      const { version } = await api(`/contents/${contentId}`);
      try {
        await api(`/contents/${contentId}`, {
          method: "PUT",
          body: JSON.stringify({ contentStatusId: statusId, version }),
        });
        return;
      } catch (error) {
        // 409: someone saved the draft between our GET and PUT — refetch and retry
        if (!(error instanceof ApiError) || error.status !== 409 || attempt === 3) throw error;
      }
    }
  }

  // --- step 2 & 3: ready to publish → sent to CMS ------------------------------

  async function pushReadyDrafts(readyId: string, sentId: string) {
    for (const draft of await listByStatus(readyId)) {
      const content = await api(`/contents/${draft.id}`);
      let cmsRecordId: string;
      try {
        cmsRecordId = await pushToCms(content); // push htmlSanitized, not html
      } catch (error) {
        console.error(`CMS push failed for ${draft.id}:`, error);
        continue; // stays in "ready to publish" — retried next tick
      }
      await rememberCmsRecord(content.id, cmsRecordId);
      await transitionTo(content.id, sentId);
    }
  }

  // --- step 4: sent to CMS → published -----------------------------------------

  async function finalizePublished(sentId: string) {
    for (const draft of await listByStatus(sentId)) {
      const cmsRecordId = await lookupCmsRecord(draft.id);
      const cmsState = await fetchCmsState(cmsRecordId);
      if (!cmsState.isPublic) continue;
      await api(`/contents/${draft.id}/publish`, {
        method: "POST",
        body: JSON.stringify({ url: cmsState.publicUrl }),
      });
    }
  }

  // --- main loop ---------------------------------------------------------------

  while (true) {
    const { readyId, sentId } = await loadStatusIds();
    await pushReadyDrafts(readyId, sentId);
    await finalizePublished(sentId);
    await new Promise((resolve) => setTimeout(resolve, 600_000)); // 10 minutes
  }
  ```

  ```python sync_drafts.py expandable theme={null}
  import os
  import time
  import requests

  API = "https://api.semji.com/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['SEMJI_API_KEY']}"}
  WORKSPACE_ID = os.environ["WORKSPACE_ID"]

  # --- status lookup -----------------------------------------------------------

  def load_status_ids():
      statuses = requests.get(
          f"{API}/workspaces/{WORKSPACE_ID}/content-statuses", headers=HEADERS,
      ).json()["data"]
      by_label = {s["label"].lower(): s["id"] for s in statuses}
      return by_label["ready to publish"], by_label["sent to cms"]

  # --- helpers -----------------------------------------------------------------

  def list_by_status(status_id):
      drafts, page = [], 1
      while True:
          response = requests.get(
              f"{API}/workspaces/{WORKSPACE_ID}/contents",
              headers=HEADERS,
              params={
                  "contentStatusId[]": status_id,
                  "sort": "-updatedAt",
                  "limit": 100,
                  "page": page,
              },
          ).json()
          drafts += response["data"]
          if not response["pagination"]["hasMore"]:
              return drafts
          page += 1

  def transition_to(content_id, status_id):
      for attempt in range(3):
          content = requests.get(f"{API}/contents/{content_id}", headers=HEADERS).json()
          response = requests.put(
              f"{API}/contents/{content_id}",
              headers=HEADERS,
              json={"contentStatusId": status_id, "version": content["version"]},
          )
          if response.status_code != 409:
              response.raise_for_status()
              return
          # 409: someone saved the draft between our GET and PUT — refetch and retry
      raise RuntimeError(f"version conflict persisted for {content_id}")

  # --- step 2 & 3: ready to publish → sent to CMS ------------------------------

  def push_ready_drafts(ready_id, sent_id):
      for draft in list_by_status(ready_id):
          content = requests.get(f"{API}/contents/{draft['id']}", headers=HEADERS).json()
          try:
              cms_record_id = push_to_cms(content)  # push htmlSanitized, not html
          except Exception as e:
              print(f"CMS push failed for {draft['id']}: {e}")
              continue  # stays in "ready to publish" — retried next tick
          remember_cms_record(content["id"], cms_record_id)  # your storage
          transition_to(content["id"], sent_id)

  # --- step 4: sent to CMS → published -----------------------------------------

  def finalize_published(sent_id):
      for draft in list_by_status(sent_id):
          cms_record_id = lookup_cms_record(draft["id"])  # your storage
          cms_state = fetch_cms_state(cms_record_id)      # your CMS client
          if not cms_state["is_public"]:
              continue
          requests.post(
              f"{API}/contents/{draft['id']}/publish",
              headers=HEADERS,
              json={"url": cms_state["public_url"]},
          ).raise_for_status()

  # --- main loop ---------------------------------------------------------------

  def run_once():
      ready_id, sent_id = load_status_ids()
      push_ready_drafts(ready_id, sent_id)
      finalize_published(sent_id)

  if __name__ == "__main__":
      while True:
          run_once()
          time.sleep(600)  # 10 minutes
  ```
</CodeGroup>

If your CMS supports webhooks, replace `finalizePublished` with a webhook handler that calls `POST /v1/contents/{contentId}/publish` directly when it receives a "post published" event.

## Push-based alternative with webhooks

The main flow assumes **your CMS decides** when an article goes live. Sometimes it's the other way around: your pages already exist in Semji with their URLs (you optimize existing content, or you create the page in the CMS first), and your editors want to drive publication **from Semji** by clicking **Mark as published**. In that scenario you don't need polling or custom statuses at all — Semji notifies you.

| | Pull (polling worker) | Push (webhook) |
| - | - | - |
| Source of truth for publication | Your CMS | Semji (*Mark as published*) |
| Publication URL | Discovered after the CMS publishes | Already known (existing pages) |
| Latency | Up to one polling interval | Seconds |
| Custom statuses needed | Two | None |
| You host | A scheduled worker | A public HTTPS endpoint |

Semji fires a `content_published` webhook when an editor clicks **Mark as published**. Configure it in **Settings > General > Add a Webhook**, then your endpoint creates or updates the article in the CMS. For the full setup, the payload, the security notes, and ready-made Make / N8n templates, see the [Content published webhook](/integrations/content-published-webhook) page.

### A minimal webhook handler

<CodeGroup>
  ```typescript webhook-handler.ts expandable theme={null}
  import express from "express";

  const API = "https://api.semji.com/v1";
  const HEADERS = { Authorization: `Bearer ${process.env.SEMJI_API_KEY}` };

  // Implement against your CMS — create or update by URL (idempotent)
  declare function upsertCmsArticle(article: {
    title: string | null;
    html: string | null;
    metaDescription: string | null;
    url: string;
  }): Promise<void>;

  const app = express();
  app.use(express.json());

  app.post("/webhooks/semji", (req, res) => {
    res.sendStatus(204); // ack immediately — Semji does not retry failed deliveries
    if (req.body?.event_type !== "content_published") return;
    handleContentPublished(req.body.data.id).catch(console.error);
  });

  async function handleContentPublished(contentId: string) {
    // Re-fetch with your API key — the webhook payload itself is not signed
    const response = await fetch(`${API}/contents/${contentId}`, { headers: HEADERS });
    if (!response.ok) return;
    const content = await response.json();
    if (!content.publishedAt) return;

    await upsertCmsArticle({
      title: content.title,
      html: content.htmlSanitized,
      metaDescription: content.metaDescription,
      url: content.page.url,
    });
  }

  app.listen(3000);
  ```

  ```python webhook_handler.py expandable theme={null}
  import os
  import threading

  import requests
  from flask import Flask, request

  API = "https://api.semji.com/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['SEMJI_API_KEY']}"}

  app = Flask(__name__)

  @app.post("/webhooks/semji")
  def semji_webhook():
      event = request.get_json(silent=True) or {}
      if event.get("event_type") == "content_published":
          threading.Thread(
              target=handle_content_published, args=(event["data"]["id"],)
          ).start()
      return "", 204  # ack immediately — Semji does not retry failed deliveries

  def handle_content_published(content_id):
      # Re-fetch with your API key — the webhook payload itself is not signed
      response = requests.get(f"{API}/contents/{content_id}", headers=HEADERS)
      if not response.ok:
          return
      content = response.json()
      if not content["publishedAt"]:
          return
      upsert_cms_article(  # your CMS client — create or update by URL (idempotent)
          title=content["title"],
          html=content["htmlSanitized"],
          meta_description=content["metaDescription"],
          url=content["page"]["url"],
      )
  ```
</CodeGroup>

<Tip>
  Since deliveries are not retried, add a safety net: periodically list recently published contents with [`GET /v1/workspaces/{workspaceId}/contents?markedAsPublishedAt[after]=…`](/api-reference/contents/list-contents-in-a-workspace) and reconcile any article your endpoint missed while it was down.

  Filter on `markedAsPublishedAt`, the timestamp of the publish action, and not on `publishedAt`. The latter is the date an author declares in the editor: it has day granularity and can be back or forward dated, which makes it unfit for an incremental window.
</Tip>

## Reference

* [List content statuses](/api-reference/workspaces/list-content-statuses)
* [List contents in a workspace](/api-reference/contents/list-contents-in-a-workspace)
* [Get content details](/api-reference/contents/get-content-details)
* [Update a content](/api-reference/contents/update-a-content)
* [Mark a content as published](/api-reference/contents/mark-a-content-as-published)
* [Rate limits](/api-reference/rate-limits)
* [Content published webhook](/integrations/content-published-webhook)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.