---
title: "Run SEO scan"
method: POST
path: "/api/apps/{app_id}/seo/scan"
---

# Run SEO scan

`POST /api/apps/{app_id}/seo/scan`

<Info>This API is in beta. Endpoints, fields, and behavior may still change, so avoid depending on it in production.</Info>

Scans the app for SEO problems and returns the [score and checklist](/developers/references/apps-api/sections/seo#the-score-and-the-checklist) in two parts. The score is a single 0-100 number with a letter grade. The checklist is the individual checks, each pass, warn, or fail, that produced the score.

The scan reads the app's own configuration and also fetches the live site over HTTP to check what it really serves for `robots.txt`, `sitemap.xml` and the home page. Those three run in parallel with a five-second timeout each and one retry, so expect a few seconds, and up to about ten when the live site is slow to answer.

An app that isn't published yet still scans, but its live checks have nothing to fetch. Rather than reporting every check that fails as a result, Base44 reports only the root cause and drops the downstream failures that just restate it, in this case that the app isn't published. The same collapsing applies when the live site's host can't be reached at all. Instead of one warning per probe, you get a single row for the host being unreachable.

Base44 also stores the result, so [Get SEO score](/api-reference/get-seo-score) and [Get last SEO scan](/api-reference/get-last-seo-scan) return this same scan until the next one runs, without needing to scan again. Storing the result is best-effort and can't fail the scan itself, so a successful call here only promises the scan that just ran, not that the two report endpoints already reflect it. Read them back if you need to be sure they're in sync.

The score and the checklist are stored separately, and two scans running at once can interleave those two writes. So if you read both report endpoints, compare their `scanned_at` before treating the numbers as one scan's.

Each check that Base44 can act on carries a `fix_action`. Read it to see what the builder offers.

The overall score isn't a simple tally of passing and failing checks. Category scores are weighted differently, and some critical checks can further cap the overall score when they fail. That's why `score.overall` doesn't add up to a plain count of failing checks, even though the `passed`, `warnings` and `failures` counts inside each category are exact. Use `failures` from [Get SEO score](/api-reference/get-seo-score) if you need the exact total across every category.

<Warning>The response includes fields beyond the ones documented here. Don't rely on undocumented response fields, as they can change at any time.</Warning>

## Path parameters

- `app_id` string, required — ID of the app to scan.

## Query parameters

- `include_ai_visibility` boolean

## Response `200`

The scan Base44 just ran.

- SEOScanReportResponse — A scan Base44 just ran.
  - `result` SEOScanReport, required — An SEO scan: its score and every check behind it.
    - `score` SEOOverallScore, required — The scan's score, overall and per category.
      - `overall` integer, required — Overall SEO score for the app, 0 to 100.
      - `grade` string, required — Letter grade for `overall`: `A`, `B`, `C`, `D` or `F`.
      - `categories` object — Per-category breakdown, keyed by the same category names the checks carry: `meta_tags`, `crawlability`, `structured_data`, `ai_discoverability` and `content_quality`, plus `indexing_performance` when Google Search Console integration is enabled for you. Base44 can add another category, so read the map rather than assuming this list is complete. Every category in the map is scored, including one this scan ran no checks for, which scores 100 with all three counts at zero.
    - `checks` SEOCheckResult[] — The checks behind the score, in the order Base44 reports them. Not every check it ran appears here. A failure that only restates an upstream one is dropped in favour of the root cause, so an unpublished or login-gated app returns the one gating failure rather than the dozen findings that follow from it.
      - `id` string, required — Identifier of the check. Stable across scans, but a check can be missing from a later scan rather than change status, because Base44 collapses cascading failures into their root cause.
      - `category` string, required — Which part of the score this check feeds: `meta_tags`, `crawlability`, `structured_data`, `ai_discoverability`, `content_quality`, or `indexing_performance` when Google Search Console integration is enabled for you.
      - `title` string, required — Short name of the check, phrased for the finding rather than the subject, so it changes with `status`.
      - `status` 'pass' | 'warn' | 'fail', required — Outcome of the check. A `warn` scores 50 against the category and a `fail` scores 0. Not every check can return all three.
      - `description` string, required — What the check found, written for the app's owner.
      - `details` unknown[] — Supporting detail for the finding, usually one object per page or entity involved. The keys differ per check, so treat the entries as opaque.
        - unknown
      - `fix_action` SEOFixAction — The remedy Base44 offers for a check, for display.
        - `type` string, required — Identifier of the action the Base44 builder offers for this check.
        - `label` string, required — Label the builder shows on the button for this action.
        - `params` object — Arguments to pass with the action. Empty on every action a scan emits today; keep reading it rather than assuming it stays empty.
      - `info_only` boolean — Environmental findings, such as DNS or hosting issues that can't be resolved from inside the app, set this to `true`. Everything else is `false`.
    - `scanned_at` string, required — When the scan ran, as an ISO 8601 UTC timestamp.

## Other responses

- `401` — Missing or invalid credentials.
- `403` — You don't have access to this app, you used a workspace API key, or you used a read-only personal access token. These endpoints accept a user's credentials only, and none of them accept a read-only credential, including the ones that only read.
- `404` — App not found.
- `422` — Validation Error
- `429` — Rate limit exceeded. The base limit is 30 requests per minute, shared with the app's other SEO endpoints. See [Rate limits](/developers/references/apps-api/get-started/rate-limits) for the multiplier your plan gets.

## Changes

- **2026-09-15** `ca10c5ce328c` — 2 info
  - added the new optional `query` request parameter `include_ai_visibility`
  - added the non-success response with the status `422`

[Change history](https://skmtc.dev/base44/apis/base44-app-management-api/changes/api/apps/:app_id/seo/scan/post.md)

---

[API](https://skmtc.dev/base44/apis/base44-app-management-api.md) · [All operations](https://skmtc.dev/base44/apis/base44-app-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/base44/apis/base44-app-management-api/revisions/cf164639a9bf?raw)
