Skip to content

REST API ​

Lumtera's admin screens and editor panels are built on these routes, so everything the UI can do is available to your own code. Authenticate as you would with any WordPress REST route, using a cookie and nonce in wp-admin, or an Application Password from outside.

Permalinks

If your site uses plain permalinks, use the ?rest_route= form, for example https://example.com/?rest_route=/lumtera/v1/site-summary. It works under every permalink setting.

When a permission check fails, WordPress answers with its usual rest_forbidden error (401 when logged out, 403 when logged in).

"Lumtera's report capability" below means the capability from the lumtera_capability filter. By default that's lumtera_view_reports, which Settings → Permissions gives to roles. See Roles & permissions.

Free: lumtera/v1 ​

RouteMethodsPurpose
/checkPOSTCheck unsaved content
/posts/{id}/issuesGETStored results for a post
/posts/{id}/scanPOSTCheck a saved post and store the result
/posts/{id}/dismissPOSTDismiss or restore an issue
/posts/{id}/manualGET, POSTManual check results
/posts/{id}/ai/link-textPOSTAI suggestions for vague link text
/posts/{id}/ai/headingsPOSTAI suggestions for headings
/posts/{id}/ai/summaryPOSTAI plain-language summary
/site-summaryGETSite-wide numbers for the Pro portfolio
/bulkPOSTNext batch of Check all content
/images/{id}POST, PUT, PATCHSave an image's alt text
/images/{id}/applyPOSTCopy alt text into posts
/images/{id}/suggestPOSTAI alt text suggestions

POST /check ​

Checks content without storing anything. This is what the editor sidebar calls as you type.

ArgumentTypeDefaultNotes
post_idint0The post being edited. Used for permissions, the reading level's language and dismissals.
contentstring''Block markup or HTML, up to 512 KB (lumtera_max_check_bytes)
block_idsstring[][]Editor block client IDs, in order, so issues can be mapped to blocks. Up to 5,000, each up to 64 characters.
elementorstring''Unsaved Elementor data (JSON), up to the same size as content

Permission: edit_post on post_id if given, otherwise edit_posts.

Rate limit: 120 checks per user per minute (lumtera_check_rate_limit). Over the limit returns HTTP 429 lumtera_rate_limited.

With elementor and a post_id, the unsaved Elementor elements are rendered and checked instead of content. That needs Elementor's own edit permission (403 lumtera_forbidden) and valid JSON (400 lumtera_bad_elements).

Response:

json
{
  "issues": [ { "rule": "link-no-name", "title": "Link has no text", "category": "links",
                "wcag": "2.4.4", "wcag_name": "Link Purpose (In Context)",
                "wcag_url": "https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html",
                "level": "A", "how_to_fix": "…", "id": "<fingerprint>", "severity": "error",
                "message": "…", "context": "<a …>", "block": "<client id>", "element": "" } ],
  "dismissed": [],
  "counts": { "error": 1, "warning": 0, "notice": 0 },
  "score": 85,
  "rules_run": 64,
  "readability": { "grade": 7.2, "words": 412, "sentences": 31, "target": 9, "lang": "en",
                   "formula": "flesch-kincaid", "formula_name": "Flesch–Kincaid grade level",
                   "score": 7.2, "scale": "grade", "label": "Easy to read" },
  "can_dismiss_errors": true
}
  • id is the issue's fingerprint: an MD5 of the check ID and the normalized markup. A repeat of the same markup in one post adds its occurrence number, so each copy has its own fingerprint. Dismissals, Pro tasks and Pro ignore rules are keyed on it.
  • readability is null when it isn't measured: for an unsupported language, or text that's too short. formula depends on the post's language (Polylang and WPML are read). scale tells how to read score.
  • dismissed lists hidden issues with the same fields, plus dismissed_by, note, time, source and can_restore. source is post for a dismissal on this post. For an issue ignored site-wide in Pro, source is global, note is the reason, and restore_path and expires are added.
  • can_dismiss_errors is false when there's no post_id.
  • Posts built with Elementor also get builder: { name, edit_url }.

GET /posts/{id}/issues ​

The stored issues for a post, from its last check. Nothing is checked again. Permission: edit_post.

Response: { "issues": [ … ], "summary": { "errors", "warnings", "notices", "dismissed", "score", "grade", "scanned_at", "version" } }. Issues have the same fields as in /check, with block empty. summary is null if the post hasn't been checked.

POST /posts/{id}/scan ​

Checks the saved post and stores the result. Returns the same shape as /check. Permission: edit_post.

Errors: 404 lumtera_not_found, 400 lumtera_not_scanned (the content type isn't checked).

POST /posts/{id}/dismiss ​

Dismisses an issue, or restores it.

ArgumentTypeRequiredNotes
fingerprintstringYes32 lowercase hex characters
rulestringYesThe check ID
severityerror, warning or noticeYes
notestringUp to 1,000 characters
contentstringUnsaved editor content, to find issues not saved yet
elementorstringUnsaved Elementor data
restorebooltrue to restore instead of dismiss

Permission: edit_post. Dismissing or restoring an error also needs the capability from lumtera_dismiss_errors_capability. By default that's lumtera_dismiss_errors, given to roles under Settings → Permissions.

The server finds the issue itself and uses its real check and severity, whatever the request says. It looks in the stored results, then the saved post, then content or elementor.

Returns { "ok": true }, then checks the post again and stores the result. Errors: 404 lumtera_not_dismissed (restoring an issue that isn't dismissed), 400 lumtera_unknown_issue, 403 lumtera_forbidden.

/posts/{id}/manual ​

Results of the manual checks for a post.

Permission: edit_post, and the post must be of a content type Lumtera checks.

GET returns the current results. POST records or clears one result:

ArgumentTypeRequiredNotes
teststringYeskeyboard, zoom-reflow, text-spacing, screen-reader, forms, media, motion-timing, hover-focus or orientation
resultstringYespass, fail, na, or '' to clear the result
notestringUp to 2,000 characters
failedstring[]With fail: the WCAG criteria that failed, such as 2.4.7. Up to 10. Empty means every criterion of the test failed.

Both methods return:

json
{
  "results": {
    "keyboard": { "result": "fail", "note": "Focus is lost in the menu.", "failed": [ "2.4.7" ],
                  "time": 1790000000, "userName": "Maya", "date": "September 25, 2026" }
  },
  "summary": { "total": 9, "done": 1, "passed": 0, "failed": 1, "na": 0 },
  "label": "Manual checks: 1 of 9 done, 1 failed"
}

Error: 400 lumtera_manual_invalid. Saving fires lumtera_manual_test_saved. See Hooks & filters.

AI writing suggestions ​

Three routes ask the AI provider connected to WordPress for writing suggestions. Each needs its switch on under Settings → AI (settings keys link_text, headings and summary) and WordPress's AI Client. See AI suggestions.

Permission: the feature is switched on, and edit_post on the post. The post must exist.

Rate limit: 30 AI requests per user in 10 minutes, shared by all AI features, alt text included (lumtera_ai_rate_limit). Over the limit returns 429 lumtera_ai_rate_limited.

All three accept title (string): the post title as typed. When empty, the saved title is used.

Suggestions are drafts. Nothing is saved: the editor shows them, and the author chooses whether to use one.

POST /posts/{id}/ai/link-text ​

ArgumentTypeRequiredNotes
textstringYesThe current link text
hrefstringThe link's address
htmlstringThe paragraph around the link
rulestringThe finding: link-ambiguous-text (default), link-url-as-text or link-same-text-different-url

Response: { "suggestions": [ "…" ], "note": "…" }, with up to three suggestions of at most 90 characters. Suggestions that repeat the current text, or would still fail the link text checks, are removed.

POST /posts/{id}/ai/headings ​

ArgumentTypeRequiredNotes
modestringYesbold (turn a bold paragraph into a heading) or outline (suggest where subheadings go)
textstringFor boldThe bold paragraph
nextstringbold: the text that follows it
previousstringbold: the heading before it
paragraphsstring[]For outlineThe post's paragraphs, in order. Up to 80 are used. At least 3 must have text.

Response for bold: { "suggestions": [ "…" ], "note": "…" }, with up to two suggestions. For outline: { "headings": [ { "index": 3, "text": "…" } ], "note": "…" }, with up to four headings. index is the zero-based position in paragraphs that the heading goes before. Headings are at most 80 characters.

POST /posts/{id}/ai/summary ​

ArgumentTypeRequiredNotes
contentstringYesThe post content (HTML or block markup), up to 512 KB. It needs at least 100 words.

Response: { "summary": "…", "words": 96, "note": "…" }. The summary is at most 120 words.

Errors for all three: 400 lumtera_ai_no_input (nothing to work with), 501 lumtera_ai_unavailable or lumtera_ai_no_model, 502 lumtera_ai_failed or lumtera_ai_bad_answer. Each route has a lumtera_ai_{feature}_pre filter to answer without the AI Client. See Hooks & filters.

GET /site-summary ​

A summary of the site, for the Pro agency portfolio. Permission: the lumtera_view_summary capability (the Lumtera Reporter role and administrators) or Lumtera's report capability.

json
{
  "name": "Crumb & Co. Bakery",
  "url": "https://example.com",
  "admin_url": "https://example.com/wp-admin/admin.php?page=lumtera",
  "version": "1.0.0",
  "totals": { "content": 12, "scanned": 12, "average": 88, "errors": 8, "warnings": 12,
              "notices": 5, "failing": 4, "unscanned": 0, "passing": 8 },
  "rules": [ { "title": "Form field has no label", "wcag": "4.1.2", "severity": "error", "issues": 3, "posts": 1 } ],
  "worst": [ { "title": "Order bread online", "errors": 3, "url": "https://example.com/wp-admin/post.php?post=42&action=edit" } ],
  "time": 1790000000
}

rules lists the five most common checks. worst lists up to five published items. No page content is included.

POST /bulk ​

Checks the next batch of content, for Check all content. Each call checks up to 50 posts, stopping early after about 2 seconds.

ArgumentTypeDefault
afterint0. The cursor from the previous response.
only_unscannedboolfalse

Permission: Lumtera's report capability. Response: { "scanned", "after", "remaining", "done", "totals" }. Call again with the returned after until done is true. Each call fires lumtera_bulk_batch_done.

POST /images/ ​

Saves an image's alt text in the Media Library. Also accepts PUT and PATCH. Permission: edit_post on the image. id must be an image.

ArgumentTypeNotes
altstringRequired
source'' or aiai records that the text began as an AI suggestion, and who accepted it

Response: { "id", "alt", "status": "missing|review|ok", "judged", "used", "empty": [ { "id", "title", "editable" } ], "manual": [ … ] }. empty lists posts showing the image without alt text that Lumtera can fill. manual lists posts where the alt text has to be added by hand.

POST /images/{id}/apply ​

Adds the image's alt text to posts that show it without any. Existing alt text is never replaced. Permission: edit_post on the image.

Response: { "updated": [ … ], "skipped": 0, "skipped_reasons": { "permission": 0, "filtered": 0 } } plus the image state. Error 400 lumtera_no_alt if the image has no alt text yet.

POST /images/{id}/suggest ​

Asks the connected AI provider for alt text suggestions. Needs AI alt text suggestions switched on. Permission: the feature is switched on, and edit_post on the image.

ArgumentTypeNotes
post_idintOptional. The post to take context from. It's ignored if the user can't read it.

Response: { "suggestions": [ "…" ], "decorative": false, "note": "…" } with up to three suggestions.

It shares the AI rate limit above. Errors: 429 lumtera_ai_rate_limited, 400 lumtera_ai_no_file (the file can't be read or is over 4 MB), 501 lumtera_ai_unavailable or lumtera_ai_no_model, 502 lumtera_ai_failed or lumtera_ai_bad_answer.

Pro: lumtera-pro/v1 Pro ​

Every Pro route also needs an active license. Otherwise its permission check fails.

Fix tracking ​

RouteMethodArgumentsPermission
/tasksGETpost_id (required)edit_post
/tasksPOSTpost_id, fingerprint (required); assignee (default 0); noteedit_post. Assigning someone else needs edit_others_posts.
/tasks/{id}POSTstatus (open, in_progress, fixed, wont_fix), assignee, noteedit_post on the task's post. wont_fix on an error needs the dismiss-errors capability.
/tasks/{id}/historyGETedit_post on the task's post
/tasks/{id}/issuePOSTedit_post on the task's post, a connected issue tracker, and Lumtera's report capability
/assigneesGETedit_posts

POST /tasks finds the issue on the server by its fingerprint: first in the stored results, then by checking the saved post. rule, severity, message and context are still accepted from older clients, but ignored. Errors: 404 lumtera_pro_finding, 400 lumtera_pro_assignee (that person can't edit the post), 403 lumtera_pro_assign_others.

GET /tasks returns an object keyed by fingerprint. A task looks like:

json
{
  "id": 7, "post_id": 42, "fingerprint": "…", "rule": "link-no-name", "title": "Link has no text",
  "wcag": "2.4.4", "severity": "error", "message": "…", "context": "…",
  "status": "in_progress", "verified": false, "status_label": "In progress",
  "assignee": 3, "assignee_name": "Maya", "updated": 1790000000,
  "post_title": "Order bread online", "edit_url": "…",
  "issue": { "tracker": "github", "tracker_name": "GitHub", "url": "https://github.com/acme/site/issues/12", "key": "#12" }
}

issue is null until an issue is created for the task in a tracker.

POST /tasks/{id}/issue creates the task's issue in the connected tracker (GitHub, GitLab, Jira or Linear) and returns the task. A task that already has an issue returns it unchanged. Errors: 404 lumtera_pro_tracker_task, 400 lumtera_pro_tracker_none, 409 lumtera_pro_tracker_busy (being created right now), 429 when the tracker asks to wait, and 502 for other tracker errors.

History entries are { "action", "detail", "user", "time" }. /assignees returns [ { "id", "name" } ]: everyone who can edit posts, or only you if you can't assign others.

Ignore rules ​

Rules that hide a finding on every page. See Ignore site-wide.

RouteMethodNotes
/ignoreGETEvery rule, newest first, including expired ones
/ignorePOSTAdds a rule. Returns it with HTTP 201.
/ignore/{id}DELETERemoves a rule. Returns { "deleted": true, "previous": { … } }.

Permission: manage_options.

ArgumentTypeRequiredNotes
rulestringYesThe check ID. Pro page check IDs are accepted too.
matchexact or patternDefault exact
valuestringYesUp to 1,000 characters. For exact: the finding's markup, as shown under Markup. For pattern: an optional tag name followed by .class and #id parts, such as a.social-link or .cookie-banner.
reasonstringYesUp to 500 characters. Shown wherever the finding is hidden.
expiresstring or intA date (Y-m-d, end of that day in the site's time zone) or a Unix time. Empty for never.
severitystringerror, warning or notice: the most severe finding the rule may hide. Empty for any.

A rule looks like:

json
{
  "id": "k3x9q2m7b1zc", "rule": "link-ambiguous-text", "match": "pattern", "value": "a.read-more",
  "severity": "", "reason": "Theme archive links; the heading gives context.",
  "user": 1, "time": 1790000000, "expires": 0,
  "check": "Link text is vague", "by": "Maya", "expired": false
}

Errors (400 unless noted): lumtera_pro_ignore_rule, lumtera_pro_ignore_match, lumtera_pro_ignore_value, lumtera_pro_ignore_pattern, lumtera_pro_ignore_reason, lumtera_pro_ignore_expiry, lumtera_pro_ignore_full (500 rules at most), 409 lumtera_pro_ignore_exists, and 404 lumtera_pro_ignore_missing on delete.

Adding or removing a rule re-checks the affected posts in the background. It also records issue.ignored_globally or issue.unignored_globally in the activity log.

Page checks ​

RouteMethodNotes
/page-checksPOSTStores a browser check. See the arguments below.
/page-checks/{id}GET, DELETERead one result, or remove it. Removing a desktop result also removes the page from scheduled checks.
/page-checks/scheduleGET, POST, PUT, PATCHRead or set frequency (off, daily, weekly; required) and templates (bool)
/page-checks/schedule/runPOSTRuns one batch of scheduled checks now

POST /page-checks arguments:

ArgumentTypeNotes
urlstringRequired. A page of this site.
htmlstringRequired. The rendered page, up to 6 MB.
titlestringUp to 300 characters
contrastarrayUp to 200 contrast measurements from the browser
viewportstringdesktop (default) or phone. A page has one stored result per viewport.
layoutarrayWith phone: up to 100 reflow and touch-target measurements taken at 390 pixels wide

A result looks like { "id", "url", "title", "errors", "warnings", "notices", "score", "checked", "source", "browser", "viewport", "issues": [ … ] }. source is browser or scheduled.

Permission: Lumtera's report capability.

Documents ​

POST /documents/scan checks PDFs. Pass id to check one file, or page through all of them with the after cursor until done is true. paged picks the page of the results table returned at the end. Permission: Lumtera's report capability. Error: 404 lumtera_pro_not_found.