Skip to content

Testing

GET /v1/organizations/:orgId/test-activity

Required role: Member.

Returns a paginated list of test activities across all non-archived products in the organization, including productName and productSlug fields.

Query parameters:

Parameter Type Description
page integer Page number (default: 1).
pageSize integer Results per page (default: 20, max: 100).
type string Comma-separated list of one or more of automated_scan, manual_test, usability_test, user_research. Filter by activity type.
productId string Filter to a specific product.
sortField "createdAt" | "startedAt" | "completedAt" Field to sort by (default: "createdAt").
sortDirection "asc" | "desc" Sort direction (default: "desc").

Response:

{
"testActivities": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 42,
"totalPages": 3
}
}

GET /v1/organizations/:orgId/products/:productSlug/test-activity

Required role: Member.

Same query parameters as the organization-level list, except productId (fixed by the URL). Each item also includes areas.

Response: Same shape as the organization-level list.


GET /v1/organizations/:orgId/products/:productSlug/testing-feed

Required role: Member.

Returns a merged, paginated, chronological stream of the product’s test activities and audits. Audits are included only when the organization also has the audits module enabled; the endpoint itself is gated on testing alone.

Query parameters:

Parameter Type Description
page integer Page number (default: 1).
pageSize integer Results per page (default: 20, max: 100).
contextTrigger string Comma-separated triggers, or none for entries with no context.
contextEnvironment string Comma-separated environment names, preview for pull request previews, or none for entries naming no environment.

Both filters drop any entry that recorded no context unless the filter includes none — every audit, plus any activity logged by hand or reported without a context object.

Response:

{
"entries": [
{
"kind": "test_activity",
"id": "...",
"number": 3,
"type": "automated_scan",
"tool": "axe",
"title": null,
"context": {
"trigger": "pull_request",
"ref": "fix/contrast",
"commitSha": "0c4de91aa7712b35",
"prNumber": 577,
"prUrl": "https://github.com/acme/shop/pull/577",
"environment": null,
"runNumber": 1287,
"runAttempt": 1
},
"externalUrl": "https://github.com/acme/shop/actions/runs/9001",
"occurredAt": "2025-06-01T00:00:00.000Z",
"discoveredIssueCount": 4,
"discoveredFeedbackCount": 0
},
{
"kind": "audit",
"id": "...",
"number": 2,
"title": null,
"status": "completed",
"occurredAt": "2025-05-20T00:00:00.000Z",
"discoveredIssueCount": 7
}
],
"tools": ["axe", "manual"],
"environments": ["production", "staging"],
"hasPreviewEnvironments": true,
"pagination": { "page": 1, "pageSize": 20, "total": 12, "totalPages": 1 }
}

occurredAt is a test activity’s completedAt (falling back to createdAt), or an audit’s completion/start date. Entries are sorted newest first.

Audit entries have no context field at all, since an audit records no run. Activity entries always carry one, null when the activity was logged by hand or reported without a context object. tools and environments are the values recorded in this scope, for building filter menus; they ignore the current query so every choice stays available. environments lists real deployments only — one recorded at least once outside a pull request — while hasPreviewEnvironments reports whether any pull request previews exist, since there is one per pull request and listing them would grow without bound.


GET /v1/organizations/:orgId/testing-feed

Required role: Member.

Same as the product-level feed, but spans every non-archived product in the organization. Entries additionally include productName and productSlug.


POST /v1/organizations/:orgId/products/:productSlug/test-activity

Required role: Member.

Cannot log an activity for an archived product — returns 409 Conflict.

Request body:

Field Type Description
type "automated_scan" | "manual_test" | "usability_test" | "user_research" Required. Kind of evaluative activity.
title string Optional custom title (max 200 characters). Defaults to a generated title from the type and product name.
tool string Optional producer slug or free text (max 120 characters).
target string Optional URL(s) or scope tested (max 2000 characters).
externalUrl string Optional URL linking to the source, e.g. a CI run.
startedAt string (ISO 8601) Optional start time.
completedAt string (ISO 8601) Optional completion time. Defaults to now.
areaIds string[] Optional area IDs this activity covered. All must belong to the product.

Activity numbers are sequential per product, assigned atomically. Logging an activity recomputes the product’s Grade.

Returns 201 Created with a Location header.


GET /v1/organizations/:orgId/products/:productSlug/test-activity/:number

Required role: Member.

:number is the sequential integer activity number, not a UUID.


GET /v1/organizations/:orgId/products/:productSlug/test-activity/:number/issues

Required role: Member.

Returns all issues linked to the specified test activity.


DELETE /v1/organizations/:orgId/products/:productSlug/test-activity/:number

Required role: Admin or owner.

Recomputes the product’s Grade. Returns 204 No Content.


POST /api/ingest/test-activity

Public endpoint for external tools (the GitHub accessibility scanner is the first intended client). Unlike every other endpoint on this page, it is not under /v1 and is not session-authenticated — it’s mounted at the app level, like the webhook receiver, and authenticated by a bearer API key instead.

Authentication:

Authorization: Bearer <API key with the ingest scope>

Returns 401 Unauthorized if the key is missing, invalid, or revoked, and 403 Forbidden if the key is valid but lacks the ingest scope. Create a key under Settings → API keys.

Rate limit: 300 requests / minute, keyed by client IP (not by key, so a rotated or invalid key can’t dodge the limit). This is separate from — and in addition to — the global rate limit, which does not apply to this path. Exceeding it returns 429 Too Many Requests.

Request body (max 1 MB, returns 413 Payload Too Large if exceeded):

Field Type Description
productRef string Product slug to attribute the activity to. Required unless the key is already scoped to a single product.
type "automated_scan" | "manual_test" | "usability_test" | "user_research" Required.
tool string Producer slug or free text (max 120 characters). Required for automated_scan, where it scopes both finding identity and auto-resolution. Optional otherwise.
title string Optional title (max 200 characters).
target string Optional URL(s) or scope tested (max 2000 characters).
externalUrl string Optional URL linking to the source.
startedAt string (ISO 8601) Optional start time.
completedAt string (ISO 8601) Optional completion time. Defaults to now.
idempotencyKey string Optional. Re-posting the same key for the same product returns the existing activity instead of creating a duplicate.
context object Optional. The run the activity came from — see below.
areas string[] Optional area slugs (max 50) to scope the activity to. All must exist on the target product.
coverage "complete" | "partial" Whether the run examined its whole scope. Defaults to partial. Only complete lets Ablebase resolve findings the run no longer reports, and only when areas is omitted; see below.
findings array Defaults to []. For automated_scan these become findings awaiting triage; for the human-run types they become issues directly.

Context fields:

The context records three separate things: what started the run, which code it evaluated, and where it ran. Only the environment says whether an activity described your live product, so a scan of a pull request preview stays distinguishable from a scan of production.

Field Type Description
trigger "pull_request" | "push" | "schedule" | "manual" | "other" Required within context. What started the run. Use other rather than guessing when none fits.
ref string Optional branch or tag name (max 255 characters).
commitSha string Optional commit that was evaluated (max 64 characters).
prNumber integer Optional pull request number.
prUrl string Optional URL of the pull request.
environment string Optional deployment the activity ran against, e.g. "production" (max 120 characters). Omit it for a CI job that scans a server it starts itself — that describes no deployment.
runNumber integer Optional run number as your CI displays it.
runAttempt integer Optional attempt number; 1 for a first run. A rerun is ingested as its own activity, so this is what tells two otherwise identical entries apart.

Finding fields:

Field Type Description
title string Required (max 300 characters).
description string Optional (max 5000 characters).
severity "high" | "medium" | "low" Optional.
impact "high" | "medium" | "low" Optional.
sourceUrl string Optional URL.
wcag string[] | string Optional WCAG success criteria, e.g. ["1.4.3"]. A rule often maps to several: axe’s video-caption covers 1.2.1, 1.2.2 and 1.2.3, so send them all. A bare string is still accepted and read as a one-element list. Each entry is trimmed and must be non-empty (max 50 characters, max 20 entries); omit the field rather than sending [].
selector string Optional CSS selector of the failing element. Part of a finding’s identity, so send the same one on every run.
ruleId string The scanner’s own stable rule identifier, e.g. axe-core’s "color-contrast" (max 200 characters). Required for automated_scan; optional otherwise.

If the key is scoped to a product and productRef names a different one, returns 403 Forbidden. If the target product is archived, returns 409 Conflict.

Response:

{
"testActivity": { "id": "...", "number": 4 },
"issueCount": 0,
"findingCount": 3,
"resolvedCount": 1,
"created": true
}
Field Description
issueCount Issues created directly. Always 0 for automated_scan, which raises findings instead.
findingCount Findings this run reported, whether newly raised or already known.
resolvedCount Findings resolved because a complete sweep no longer reports them. Always 0 unless coverage is complete and the report omits areas.

201 Created for a new activity, 200 OK when idempotencyKey matched an existing one (created: false; findings are not re-applied on replay).