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
| Route | Methods | Purpose |
|---|---|---|
/check | POST | Check unsaved content |
/posts/{id}/issues | GET | Stored results for a post |
/posts/{id}/scan | POST | Check a saved post and store the result |
/posts/{id}/dismiss | POST | Dismiss or restore an issue |
/posts/{id}/manual | GET, POST | Manual check results |
/posts/{id}/ai/link-text | POST | AI suggestions for vague link text |
/posts/{id}/ai/headings | POST | AI suggestions for headings |
/posts/{id}/ai/summary | POST | AI plain-language summary |
/site-summary | GET | Site-wide numbers for the Pro portfolio |
/bulk | POST | Next batch of Check all content |
/images/{id} | POST, PUT, PATCH | Save an image's alt text |
/images/{id}/apply | POST | Copy alt text into posts |
/images/{id}/suggest | POST | AI alt text suggestions |
POST /check
Checks content without storing anything. This is what the editor sidebar calls as you type.
| Argument | Type | Default | Notes |
|---|---|---|---|
post_id | int | 0 | The post being edited. Used for permissions, the reading level's language and dismissals. |
content | string | '' | Block markup or HTML, up to 512 KB (lumtera_max_check_bytes) |
block_ids | string[] | [] | Editor block client IDs, in order, so issues can be mapped to blocks. Up to 5,000, each up to 64 characters. |
elementor | string | '' | 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:
{
"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
}idis 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.readabilityisnullwhen it isn't measured: for an unsupported language, or text that's too short.formuladepends on the post's language (Polylang and WPML are read).scaletells how to readscore.dismissedlists hidden issues with the same fields, plusdismissed_by,note,time,sourceandcan_restore.sourceispostfor a dismissal on this post. For an issue ignored site-wide in Pro,sourceisglobal,noteis the reason, andrestore_pathandexpiresare added.can_dismiss_errorsisfalsewhen there's nopost_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.
| Argument | Type | Required | Notes |
|---|---|---|---|
fingerprint | string | Yes | 32 lowercase hex characters |
rule | string | Yes | The check ID |
severity | error, warning or notice | Yes | |
note | string | Up to 1,000 characters | |
content | string | Unsaved editor content, to find issues not saved yet | |
elementor | string | Unsaved Elementor data | |
restore | bool | true 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:
| Argument | Type | Required | Notes |
|---|---|---|---|
test | string | Yes | keyboard, zoom-reflow, text-spacing, screen-reader, forms, media, motion-timing, hover-focus or orientation |
result | string | Yes | pass, fail, na, or '' to clear the result |
note | string | Up to 2,000 characters | |
failed | string[] | 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:
{
"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
| Argument | Type | Required | Notes |
|---|---|---|---|
text | string | Yes | The current link text |
href | string | The link's address | |
html | string | The paragraph around the link | |
rule | string | The 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
| Argument | Type | Required | Notes |
|---|---|---|---|
mode | string | Yes | bold (turn a bold paragraph into a heading) or outline (suggest where subheadings go) |
text | string | For bold | The bold paragraph |
next | string | bold: the text that follows it | |
previous | string | bold: the heading before it | |
paragraphs | string[] | For outline | The 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
| Argument | Type | Required | Notes |
|---|---|---|---|
content | string | Yes | The 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.
{
"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.
| Argument | Type | Default |
|---|---|---|
after | int | 0. The cursor from the previous response. |
only_unscanned | bool | false |
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.
| Argument | Type | Notes |
|---|---|---|
alt | string | Required |
source | '' or ai | ai 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.
| Argument | Type | Notes |
|---|---|---|
post_id | int | Optional. 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
| Route | Method | Arguments | Permission |
|---|---|---|---|
/tasks | GET | post_id (required) | edit_post |
/tasks | POST | post_id, fingerprint (required); assignee (default 0); note | edit_post. Assigning someone else needs edit_others_posts. |
/tasks/{id} | POST | status (open, in_progress, fixed, wont_fix), assignee, note | edit_post on the task's post. wont_fix on an error needs the dismiss-errors capability. |
/tasks/{id}/history | GET | edit_post on the task's post | |
/tasks/{id}/issue | POST | edit_post on the task's post, a connected issue tracker, and Lumtera's report capability | |
/assignees | GET | edit_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:
{
"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.
| Route | Method | Notes |
|---|---|---|
/ignore | GET | Every rule, newest first, including expired ones |
/ignore | POST | Adds a rule. Returns it with HTTP 201. |
/ignore/{id} | DELETE | Removes a rule. Returns { "deleted": true, "previous": { … } }. |
Permission: manage_options.
| Argument | Type | Required | Notes |
|---|---|---|---|
rule | string | Yes | The check ID. Pro page check IDs are accepted too. |
match | exact or pattern | Default exact | |
value | string | Yes | Up 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. |
reason | string | Yes | Up to 500 characters. Shown wherever the finding is hidden. |
expires | string or int | A date (Y-m-d, end of that day in the site's time zone) or a Unix time. Empty for never. | |
severity | string | error, warning or notice: the most severe finding the rule may hide. Empty for any. |
A rule looks like:
{
"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
| Route | Method | Notes |
|---|---|---|
/page-checks | POST | Stores a browser check. See the arguments below. |
/page-checks/{id} | GET, DELETE | Read one result, or remove it. Removing a desktop result also removes the page from scheduled checks. |
/page-checks/schedule | GET, POST, PUT, PATCH | Read or set frequency (off, daily, weekly; required) and templates (bool) |
/page-checks/schedule/run | POST | Runs one batch of scheduled checks now |
POST /page-checks arguments:
| Argument | Type | Notes |
|---|---|---|
url | string | Required. A page of this site. |
html | string | Required. The rendered page, up to 6 MB. |
title | string | Up to 300 characters |
contrast | array | Up to 200 contrast measurements from the browser |
viewport | string | desktop (default) or phone. A page has one stored result per viewport. |
layout | array | With 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.