curl --request POST \
--url https://app.base44.com/api/apps/{app_id}/seo/scan \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.base44.com/api/apps/{app_id}/seo/scan"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.base44.com/api/apps/{app_id}/seo/scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.base44.com/api/apps/{app_id}/seo/scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.base44.com/api/apps/{app_id}/seo/scan"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.base44.com/api/apps/{app_id}/seo/scan")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.base44.com/api/apps/{app_id}/seo/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"result": {
"score": {
"overall": 82,
"grade": "B",
"categories": {
"meta_tags": {
"failures": 0,
"passed": 4,
"score": 80,
"warnings": 1
}
}
},
"scanned_at": "2026-09-03T09:15:00+00:00",
"checks": [
{
"category": "ai_discoverability",
"description": "Turn on the AI site guide so AI engines can understand your app.",
"id": "llms_txt",
"status": "warn",
"title": "AI-readable site guide is off"
}
]
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Run SEO scan
Scans the app for SEO problems and returns the score and 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 and 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, which you can apply with Apply SEO fix.
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 if you need the exact total across every category.
curl --request POST \
--url https://app.base44.com/api/apps/{app_id}/seo/scan \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.base44.com/api/apps/{app_id}/seo/scan"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.base44.com/api/apps/{app_id}/seo/scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.base44.com/api/apps/{app_id}/seo/scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.base44.com/api/apps/{app_id}/seo/scan"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.base44.com/api/apps/{app_id}/seo/scan")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.base44.com/api/apps/{app_id}/seo/scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"result": {
"score": {
"overall": 82,
"grade": "B",
"categories": {
"meta_tags": {
"failures": 0,
"passed": 4,
"score": 80,
"warnings": 1
}
}
},
"scanned_at": "2026-09-03T09:15:00+00:00",
"checks": [
{
"category": "ai_discoverability",
"description": "Turn on the AI site guide so AI engines can understand your app.",
"id": "llms_txt",
"status": "warn",
"title": "AI-readable site guide is off"
}
]
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}Authorizations
Personal access token, sent as Authorization: Bearer <token>. Each operation lists the permission it needs: read_only or full_access. A full-access token can call every operation. A read-only token can call only the operations that list read_only; anything else returns 403 read_only_credential_route_not_allowed.
Path Parameters
ID of the app to scan.
Query Parameters
Response
The scan Base44 just ran.
A scan Base44 just ran.
The scan Base44 just ran.
Show child attributes
Show child attributes
Was this page helpful?