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

# Common Tasks

> Practical Kolaria API examples: update a post, generate content, read GEO visibility and trigger a scan.

## Update an existing post

Use `PATCH /v1/posts/{postId}` to update a post. Send at least one of `title` (up to 120 characters), `slug` (lowercase letters, numbers and hyphens, up to 160 characters, or `null` to clear it), `markdown` (up to 100,000 characters) or `status` (`draft` or `published`). The response contains the updated post.

<CodeGroup>
  ```typescript fetch theme={null}
  type UpdatePostRequest = {
    title?: string;
    slug?: string | null;
    markdown?: string;
    status?: "draft" | "published";
  };

  type Post = {
    id: string;
    title: string;
    slug: string | null;
    content: string;
    htmlUrl: string | null;
    markdown: string | null;
    rawHtml: string | null;
    recommendations: string | null;
    contentType:
      | "changelog"
      | "linkedin_post"
      | "twitter_post"
      | "blog_post"
      | "investor_update"
      | "image";
    sourceMetadata: unknown;
    status: "draft" | "published";
    createdAt: string;
    updatedAt: string;
  };

  type UpdatePostResponse = {
    organization: {
      id: string;
      slug: string;
      name: string;
      logo: string | null;
    };
    post: Post;
  };

  async function updatePost(
    postId: string,
    updates: UpdatePostRequest
  ): Promise<UpdatePostResponse> {
    const response = await fetch(`https://api.kolaria.com/v1/posts/${postId}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.KOLARIA_API_KEY}`,
      },
      body: JSON.stringify(updates),
    });

    if (!response.ok) {
      throw new Error(`Failed to update post: ${response.status}`);
    }

    const data: UpdatePostResponse = await response.json();
    return data;
  }
  ```

  ```bash cURL theme={null}
  curl -X PATCH "https://api.kolaria.com/v1/posts/post_abc" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Ship notes for week 11",
      "markdown": "# Ship notes\n\nWe shipped a faster editor.",
      "status": "published"
    }'
  ```

  ```typescript SDK theme={null}
  const updated = await notra.content.updatePost({
    postId: "post_abc",
    body: {
      title: "Ship notes for week 11",
      markdown: "# Ship notes\n\nWe shipped a faster editor.",
      status: "published",
    },
  });

  console.log(updated.post);
  ```
</CodeGroup>

`PATCH /v1/posts/{postId}` is limited to 60 requests per minute per key. Sending a body with none of the four fields returns `400`.

## Create a new post

There is no `POST /v1/posts` endpoint for creating a post with arbitrary fields. Posts are created by generation: queue a job with `POST /v1/posts/generate`, then poll it until it reports a `postId`.

<Warning>
  If a guide or AI answer suggests `POST /v1/posts` or an `author` field for post creation, that information is not correct for the current Kolaria API.
</Warning>

### Queue a generation job

`contentType` is required (`changelog`, `blog_post`, `linkedin_post`, `twitter_post` or `image`). `lookbackWindow` defaults to `last_7_days` (`current_day`, `yesterday`, `last_7_days`, `last_14_days`, `last_30_days`). Point the job at connected sources with `integrations.github` and `integrations.linear` (integration ids from `GET /v1/integrations`) or at public repositories with `github.repositories`. When no selector is given, every connected GitHub integration is used.

```bash theme={null}
curl -X POST "https://api.kolaria.com/v1/posts/generate" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contentType": "changelog",
    "lookbackWindow": "last_7_days",
    "integrations": { "github": ["integration_1"] },
    "dataPoints": {
      "includePullRequests": true,
      "includeCommits": true,
      "includeReleases": true,
      "includeLinearData": false
    }
  }'
```

The API answers `202 Accepted` with the job:

```json theme={null}
{
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null },
  "job": {
    "id": "job_123",
    "organizationId": "org_123",
    "status": "queued",
    "contentType": "changelog",
    "lookbackWindow": "last_7_days",
    "repositoryIds": [],
    "brandVoiceId": null,
    "workflowRunId": null,
    "postId": null,
    "error": null,
    "source": "api",
    "createdAt": "2026-09-02T10:00:00.000Z",
    "updatedAt": "2026-09-02T10:00:00.000Z",
    "completedAt": null
  }
}
```

### Poll the job

```typescript theme={null}
type GenerationJob = {
  id: string;
  status: "queued" | "running" | "completed" | "failed" | "skipped";
  postId: string | null;
  error: string | null;
};

type GenerationStatusResponse = {
  job: GenerationJob;
  events: Array<{ type: string; message: string; createdAt: string }>;
};

async function waitForPost(jobId: string): Promise<string> {
  while (true) {
    const response = await fetch(
      `https://api.kolaria.com/v1/posts/generate/${jobId}`,
      { headers: { Authorization: `Bearer ${process.env.KOLARIA_API_KEY}` } }
    );
    if (!response.ok) {
      throw new Error(
        `Polling failed: ${response.status} ${await response.text()}`
      );
    }
    const data: GenerationStatusResponse = await response.json();

    if (data.job.status === "completed" && data.job.postId) {
      return data.job.postId;
    }
    if (data.job.status === "failed" || data.job.status === "skipped") {
      throw new Error(data.job.error ?? `Generation ${data.job.status}`);
    }

    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
}
```

`skipped` means the job ended without creating a post. Kolaria does not send a webhook when a job finishes, so polling is the way to find out. `POST /v1/posts/generate` is limited to 10 requests per minute per key and needs an active paid plan or AI credits.

## Read GEO visibility for a project

GEO data is scoped to a project. List projects first, then read the mention rate per engine for the last 30 days.

```bash theme={null}
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.kolaria.com/v1/projects"
```

```json theme={null}
{
  "projects": [
    { "id": "proj_123", "name": "Acme", "brandSettingsId": "brand_123", "createdAt": "2026-08-01T09:00:00.000Z" }
  ],
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

```bash theme={null}
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.kolaria.com/v1/projects/proj_123/geo/visibility/overview?days=30"
```

```json theme={null}
{
  "configured": true,
  "engines": [
    {
      "engine": "chatgpt",
      "checks": 240,
      "mentions": 96,
      "mentionRate": 0.4,
      "avgPosition": 2.1,
      "lastCheckedAt": "2026-09-02T06:00:00.000Z"
    }
  ],
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

Visibility and traffic reads accept either `days` (1 to 365, rolling window) or an explicit `from` and `to` (`YYYY-MM-DD`). When `from`/`to` are set, `days` is ignored. `configured: false` means the project has no GEO settings yet; configure it with `PATCH /v1/projects/{projectId}/geo/settings`.

These endpoints need `projects.read` and `visibility.read` plus a GEO plan. Without the plan the API returns `402`.

## Trigger a GEO scan

A scan checks every tracked prompt against every enabled engine. It takes no request body.

```bash theme={null}
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.kolaria.com/v1/projects/proj_123/geo/scans"
```

```json theme={null}
{
  "scanId": "scan_123",
  "statusUrl": "https://api.kolaria.com/v1/projects/proj_123/geo/scans/scan_123",
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

Poll `statusUrl` until `scan.status` is `completed` or `failed`. Scans are limited to 4 per hour per organization, so poll rather than re-trigger. See [Rate Limits](/api/rate-limits#geo).

## Submit feedback from an integration

Agents post to your public feedback URL without a key. Server-side code that already holds a key with `feedback.write` can use `POST /v1/feedback` instead:

```bash theme={null}
curl -X POST "https://api.kolaria.com/v1/feedback" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Export to CSV fails for reports over 10k rows.",
    "kind": "bug",
    "source": "api",
    "idempotencyKey": "ticket-4821"
  }'
```

See [Agent Feedback](/api/agent-feedback) for the full field list and the MCP tool.
