API documentation
Reference of the Pandus REST API v1: authentication, limits, errors, pagination, every endpoint with parameters and examples in curl, JavaScript, PHP and Python, and signed webhooks.
- Version:
- 1.1
- Base URL:
- https://pandus.online/api/v1
- Updated:
OpenAPI specification (YAML) What the API is for
The API returns the results of an automated check, not a full audit. It is not legal advice.
Getting started
The Pandus API gives you the automated accessibility checks of the cabinet from your own tools: a deployment script, an agency dashboard, a ticket system. Every answer is an automated check, not a full audit, and not legal advice.
- The API is included in the Pro and Agency plans, see pricing.
- In the cabinet, open API and webhooks and create a token. Copy it right away: it is shown only once.
- Keep the token on your server, for example in the environment variable
PANDUS_TOKEN, and send your first request:
curl https://pandus.online/api/v1/account \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
The answer shows your plan, its limits and how much of them is used. From there, a typical integration adds a site (POST /sites), waits for the check (GET /scans/{scan} or the scan.completed webhook) and reads the report as JSON or PDF.
Every file of a check is in the API too: the PDF report, the issue exports (CSV, JSON, GitHub issues, Jira import) and, on Agency plans, the ACR draft as JSON, HTML or PDF. GET /sites/{site}/export returns the file of the site's latest completed check without looking the check up first. PDFs are made synchronously, up to a minute the first time: keep the read timeout of your client at 120 seconds.
All addresses below are relative to the base URL https://pandus.online/api/v1. Requests and answers are JSON in UTF-8 (the PDF, the screenshots and the exports are files); dates are ISO 8601 with a time zone. The machine-readable description of everything on this page is the OpenAPI specification.
Authentication
Send the token in the Authorization header of every request:
Authorization: Bearer 12|pNd4sY…
Accept: application/json
- A token belongs to the team, not to a person, so it keeps working when someone leaves the team.
- Abilities.
readcovers everyGETrequest.writecovers adding and deleting sites and starting checks. A read-only token is enough for a dashboard. - Revoking. Delete a token in the cabinet and it stops working at once. A session cookie of the cabinet is never accepted by the API.
- Plan. If the team moves to a plan without the API, its tokens answer
403until the plan includes the API again; they are not deleted. - Keep it secret. Do not put the token in code that runs in a browser or in a public repository. Use one token per integration, so you can revoke one without touching the others.
Limits
- Requests: 60 a minute per token. The PDF report, the issue exports and the ACR files: 20 a minute.
- Every answer carries
X-RateLimit-LimitandX-RateLimit-Remaining. Above the limit you get429withRetry-After(seconds to wait) andX-RateLimit-Reset(Unix time). - Plan limits apply as in the cabinet: the number of sites, pages per check, how long checks are kept (history) and how often a check may be started by hand: once an hour per site, never while a check of the site is running. A request over a limit returns
422with a message. GET /accountreturns the limits and the usage, includingmanual_checks_available: the number of sites on which a check by hand can start right now.
Errors
Errors are JSON with a message in English. Validation errors and plan limits (422) add errors with the problems per field:
{
"message": "The url field is required. (and 1 more error)",
"errors": {
"url": ["The url field is required."],
"report_locale": ["The report locale field is required."]
}
}
| Status | Meaning |
|---|---|
401 |
No token, or the token is unknown or deleted. |
403 |
The token lacks the ability, the plan does not include the API or the tool, or the team is blocked. |
404 |
Not found in your team, or older than the plan's history. The body is always {"message": "Not found."}. |
409 |
The check is not completed yet. |
422 |
The request is not valid, or a plan limit stops it. |
429 |
Too many requests: wait Retry-After seconds. |
503 |
The PDF could not be prepared right now: try again in a minute. |
Decide by the status code, not by the wording of message: the wording may improve.
Pagination
Lists (GET /sites and GET /sites/{site}/scans) come in pages. per_page sets the size, from 1 to 100 (50 sites or 20 checks by default), page the page number. Every list has three keys:
data: the items of this page;links:first,last,prevandnext, full URLs ornull;meta:current_page,last_page,per_page,total,from,toandpath.
To read everything, follow links.next until it is null:
let url = 'https://pandus.online/api/v1/sites?per_page=100';
const sites = [];
while (url) {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
const page = await response.json();
sites.push(...page.data);
url = page.links.next;
}
Webhooks
Webhooks tell your server when something happens, so you do not have to poll. Add an endpoint in the cabinet under API and webhooks: a public https address and the events it listens to. A team can have up to 5 endpoints. The secret (whsec_…) is shown once, when the endpoint is created.
- Delivery. A
POSTwith a JSON body and the headersPandus-Event,Pandus-DeliveryandPandus-Signature. Redirects are not followed; private and local addresses are refused. - Answer with any
2xxwithin 10 seconds. Do the slow work after answering, for example in a queue. - Retries. Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.
Pandus-Deliverystays the same on every retry: use it to ignore repeats. After 20 deliveries that failed every attempt, the endpoint is switched off; switch it on again in the cabinet. - Signature.
Pandus-Signature: t=<unix time>,v1=<hex>.v1is HMAC-SHA256 of the string"<t>.<raw body>"with the endpoint's secret. Compute it over the raw body, before any JSON parsing, compare in constant time and refuse timestamps older than 5 minutes.
The events, their payloads and the signature check in four languages follow.
scan.completed · A check is completed
Sent for every completed check of a site of the team, scheduled or started by hand. A failed check sends no event. data.scan.links has ready-to-use API links to the report, the PDF, every export and the ACR draft.
Headers
| Name | Description |
|---|---|
| Pandus-Event | The event type, the same as type in the body. |
| Pandus-Delivery | ID of this delivery; the same on every retry, so you can ignore repeats. |
| Pandus-Signature | t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret (whsec_…). Compare it in constant time and refuse old timestamps (for example older than 5 minutes) against replays. |
Payload
{
"id": "evt_01j9z4c3x5v7b9n1m3k5j7h9g2",
"type": "scan.completed",
"created_at": "2026-10-14T06:03:42+00:00",
"data": {
"scan": {
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"kind": "scheduled",
"status": "completed",
"pages_requested": 2000,
"pages_scanned": 37,
"issues": 12,
"previous_issues": 15,
"by_impact": {
"critical": 2,
"serious": 7,
"moderate": 3
},
"error": null,
"queued_at": "2026-10-14T06:00:02+00:00",
"started_at": "2026-10-14T06:00:05+00:00",
"finished_at": "2026-10-14T06:03:41+00:00",
"links": {
"self": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8",
"report": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report",
"pdf": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf",
"exports": {
"json": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=json",
"csv": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv",
"github": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=github",
"jira": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=jira"
},
"acr": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr"
}
},
"site": {
"id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"url": "https://shop.example/",
"domain": "shop.example",
"name": "Example Shop",
"platform": "shopify",
"stack": [
"shopify"
],
"report_locale": "de",
"status": "active",
"tags": [
"client-a"
],
"next_scan_at": "2026-10-21T06:00:00+00:00",
"created_at": "2026-09-02T12:31:08+00:00"
},
"new_issues": 2
}
}
Any 2xx answer within 10 seconds marks the delivery as received. Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts). After 20 deliveries that failed every attempt the endpoint is switched off.
issues.new · A check found new problems
Sent after scan.completed when the check found problems that the previous check did not have (new_issues > 0). Same payload.
Headers
| Name | Description |
|---|---|
| Pandus-Event | The event type, the same as type in the body. |
| Pandus-Delivery | ID of this delivery; the same on every retry, so you can ignore repeats. |
| Pandus-Signature | t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret (whsec_…). Compare it in constant time and refuse old timestamps (for example older than 5 minutes) against replays. |
Payload
{
"id": "evt_01j9z4c3x5v7b9n1m3k5j7h9g3",
"type": "issues.new",
"created_at": "2026-10-14T06:03:42+00:00",
"data": {
"scan": {
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"kind": "scheduled",
"status": "completed",
"pages_requested": 2000,
"pages_scanned": 37,
"issues": 12,
"previous_issues": 15,
"by_impact": {
"critical": 2,
"serious": 7,
"moderate": 3
},
"error": null,
"queued_at": "2026-10-14T06:00:02+00:00",
"started_at": "2026-10-14T06:00:05+00:00",
"finished_at": "2026-10-14T06:03:41+00:00",
"links": {
"self": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8",
"report": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report",
"pdf": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf",
"exports": {
"json": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=json",
"csv": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv",
"github": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=github",
"jira": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=jira"
},
"acr": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr"
}
},
"site": {
"id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"url": "https://shop.example/",
"domain": "shop.example",
"name": "Example Shop",
"platform": "shopify",
"stack": [
"shopify"
],
"report_locale": "de",
"status": "active",
"tags": [
"client-a"
],
"next_scan_at": "2026-10-21T06:00:00+00:00",
"created_at": "2026-09-02T12:31:08+00:00"
},
"new_issues": 2
}
}
Any 2xx answer within 10 seconds marks the delivery as received. Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts). After 20 deliveries that failed every attempt the endpoint is switched off.
ping · A test event
Sent when you press "Send a test event" next to an endpoint in the cabinet. Every endpoint receives it, whatever events it listens to.
Headers
| Name | Description |
|---|---|
| Pandus-Event | The event type, the same as type in the body. |
| Pandus-Delivery | ID of this delivery; the same on every retry, so you can ignore repeats. |
| Pandus-Signature | t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret (whsec_…). Compare it in constant time and refuse old timestamps (for example older than 5 minutes) against replays. |
Payload
{
"id": "evt_01j9z5d9k1l3m5n7b9v1c3x5z7",
"type": "ping",
"created_at": "2026-10-14T09:20:11+00:00",
"data": {
"message": "Test event from Pandus."
}
}
Any 2xx answer within 10 seconds marks the delivery as received. Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts). After 20 deliveries that failed every attempt the endpoint is switched off.
Verify the signature
Compute HMAC-SHA256 of the timestamp, a dot and the raw body with the endpoint's secret, compare it with v1 in constant time and refuse timestamps older than five minutes.
Shell
# SIGNATURE: the Pandus-Signature header; body.json: the raw request body.
t=$(printf '%s' "$SIGNATURE" | sed -E 's/.*t=([0-9]+).*/\1/')
v1=$(printf '%s' "$SIGNATURE" | sed -E 's/.*v1=([0-9a-f]+).*/\1/')
expected=$({ printf '%s.' "$t"; cat body.json; } \
| openssl dgst -sha256 -hmac "$PANDUS_WEBHOOK_SECRET" -r | cut -d' ' -f1)
[ "$expected" = "$v1" ] && echo "valid" || echo "invalid"
JavaScript
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// The signature covers the raw body: read it before any JSON parser does.
app.post('/pandus/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const parts = Object.fromEntries(
(req.get('Pandus-Signature') ?? '').split(',').map((part) => part.split('=')),
);
const expected = crypto
.createHmac('sha256', process.env.PANDUS_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid =
fresh &&
typeof parts.v1 === 'string' &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
// event.type: "scan.completed", "issues.new" or "ping"
res.sendStatus(204);
});
PHP
// routes/api.php: no CSRF token on this route.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('pandus/webhook', function (Request $request) {
parse_str(str_replace(',', '&', (string) $request->header('Pandus-Signature')), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '').'.'.$request->getContent(), config('services.pandus.webhook_secret'));
abort_unless(
isset($parts['t'], $parts['v1'])
&& abs(time() - (int) $parts['t']) < 300
&& hash_equals($expected, (string) $parts['v1']),
400,
);
$event = $request->json()->all();
// $event['type']: scan.completed, issues.new or ping
return response()->noContent();
});
Python
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/pandus/webhook")
def pandus_webhook():
header = request.headers.get("Pandus-Signature", "")
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
body = request.get_data() # the raw body, before any parsing
secret = os.environ["PANDUS_WEBHOOK_SECRET"].encode()
signed = parts.get("t", "").encode() + b"." + body
expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
fresh = parts.get("t", "").isdigit() and abs(time.time() - int(parts["t"])) < 300
if not fresh or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
# event["type"]: "scan.completed", "issues.new" or "ping"
return "", 204
Account
The team the token belongs to, its plan, limits and usage.
Plan, limits and usage of the team
GET
/account
Token ability: read
The team the token belongs to with its plan, what the plan includes and how much of it is used. Check usage before you add a site or start a check.
Response
200 The team, plan, limits and usage.
application/json
{
"data": {
"team": {
"id": "01j9z3k8m4q2w7e5r6t8y0u1i2",
"name": "Nova Studio"
},
"plan": {
"code": "agency_10",
"name": "Agency 10",
"status": "active",
"ends_at": "2026-11-14T09:00:00+00:00",
"trial_ends_at": null
},
"features": {
"api": true,
"exports": true,
"white_label": true,
"statement_drafts": true,
"acr_drafts": true
},
"limits": {
"sites": 10,
"pages_per_check": 2000,
"check_frequency": "weekly",
"history_months": 24,
"seats": 5,
"manual_check_interval": "PT1H",
"requests_per_minute": 60
},
"usage": {
"sites": {
"used": 4,
"allowed": 10
},
"seats": {
"used": 2,
"allowed": 5
},
"manual_checks_available": 3
}
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/account" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/account', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/account')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/account",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Sites
Sites of the team. Adding one starts its first check.
List the team's sites
GET
/sites
Token ability: read
Sites in alphabetical order of their domain, 50 a page by default.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| tag | query | string | Only sites with this tag, e.g. the name of an agency's client. |
| per_page | query | integer (1–100) |
Items per page, 1 to 100.
Default: 50 |
| page | query | integer |
Page number, from 1.
Default: 1 |
Response
200 One page of sites.
application/json
{
"data": [
{
"id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"url": "https://shop.example/",
"domain": "shop.example",
"name": "Example Shop",
"platform": "shopify",
"stack": [
"shopify"
],
"report_locale": "de",
"status": "active",
"tags": [
"client-a"
],
"next_scan_at": "2026-10-21T06:00:00+00:00",
"created_at": "2026-09-02T12:31:08+00:00"
}
],
"links": {
"first": "https://pandus.online/api/v1/sites?page=1",
"last": "https://pandus.online/api/v1/sites?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [
{
"url": null,
"label": "« Previous",
"page": null,
"active": false
},
{
"url": "https://pandus.online/api/v1/sites?page=1",
"label": "1",
"page": 1,
"active": true
},
{
"url": null,
"label": "Next »",
"page": null,
"active": false
}
],
"path": "https://pandus.online/api/v1/sites",
"per_page": 50,
"to": 1,
"total": 1
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/sites?tag=client-a" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites?tag=client-a', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/sites', [
'tag' => 'client-a',
])
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/sites",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
params={
"tag": "client-a",
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Add a site
POST
/sites
Token ability: write
Adds a site to the team and queues its first check, unless scan is false. The address must be public: private networks, local addresses and blocked domains are refused with 422. The number of sites is limited by the plan.
Request body application/json
| Field | Type | Description |
|---|---|---|
| url(required) | string (max. 500) |
The address; https:// is added when left out.
|
| name | string | null (max. 120) | |
| report_locale(required) | string |
Values: en, uk, de, pl, cs, ro, ru |
| platform | string | null |
One platform key (older form of stack).
|
| stack | array of string (max. 8) |
Platform keys, e.g. shopify, woocommerce, wordpress, laravel, vue. Fixing tips follow the first shop platform.
|
| tags | array of string (max. 40) (max. 10) | |
| scan | boolean |
Start the first check right away.
Default: true |
Example
{
"url": "shop.example",
"name": "Example Shop",
"report_locale": "de",
"stack": [
"shopify"
],
"tags": [
"client-a"
],
"scan": true
}
Response
201 The new site.
application/json
{
"data": {
"id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"url": "https://shop.example",
"domain": "shop.example",
"name": "Example Shop",
"platform": "shopify",
"stack": [
"shopify"
],
"report_locale": "de",
"status": "active",
"tags": [
"client-a"
],
"next_scan_at": "2026-10-21T06:00:00+00:00",
"created_at": "2026-10-14T09:12:44+00:00"
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 422 | The request is not valid or a plan limit stops it. | The url field is required. (and 1 more error) |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl -X POST "https://pandus.online/api/v1/sites" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "shop.example",
"name": "Example Shop",
"report_locale": "de",
"stack": [
"shopify"
],
"tags": [
"client-a"
],
"scan": true
}'
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'shop.example',
name: 'Example Shop',
report_locale: 'de',
stack: ['shopify'],
tags: ['client-a'],
scan: true,
}),
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->post('https://pandus.online/api/v1/sites', [
'url' => 'shop.example',
'name' => 'Example Shop',
'report_locale' => 'de',
'stack' => ['shopify'],
'tags' => ['client-a'],
'scan' => true,
])
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.post(
"https://pandus.online/api/v1/sites",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
json={
"url": "shop.example",
"name": "Example Shop",
"report_locale": "de",
"stack": ["shopify"],
"tags": ["client-a"],
"scan": True,
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
One site
GET
/sites/{site}
Token ability: read
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| site(required) | path | string |
The site's id.
|
Response
200 The site.
application/json
{
"data": {
"id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"url": "https://shop.example/",
"domain": "shop.example",
"name": "Example Shop",
"platform": "shopify",
"stack": [
"shopify"
],
"report_locale": "de",
"status": "active",
"tags": [
"client-a"
],
"next_scan_at": "2026-10-21T06:00:00+00:00",
"created_at": "2026-09-02T12:31:08+00:00"
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Delete a site
DELETE
/sites/{site}
Token ability: write
Deletes the site with all its checks, reports and screenshots. This cannot be undone.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| site(required) | path | string |
The site's id.
|
Response
204 Deleted; no body.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl -X DELETE "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
// 204 No Content
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->delete('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1')
->throw();
// 204 No Content
Python
import os
import requests
response = requests.delete(
"https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
# 204 No Content
Checks
Start a check and follow its status.
Checks of a site
GET
/sites/{site}/scans
Token ability: read
Checks within the plan's history, newest first, 20 a page by default. The latest completed check of a site stays available even when it is older than the history.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| site(required) | path | string |
The site's id.
|
| per_page | query | integer (1–100) |
Items per page, 1 to 100.
Default: 20 |
| page | query | integer |
Page number, from 1.
Default: 1 |
Response
200 One page of checks.
application/json
{
"data": [
{
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"kind": "scheduled",
"status": "completed",
"pages_requested": 2000,
"pages_scanned": 37,
"issues": 12,
"previous_issues": 15,
"by_impact": {
"critical": 2,
"serious": 7,
"moderate": 3
},
"error": null,
"queued_at": "2026-10-14T06:00:02+00:00",
"started_at": "2026-10-14T06:00:05+00:00",
"finished_at": "2026-10-14T06:03:41+00:00",
"links": {
"self": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8",
"report": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report",
"pdf": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf",
"exports": {
"json": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=json",
"csv": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv",
"github": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=github",
"jira": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=jira"
},
"acr": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr"
}
}
],
"links": {
"first": "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans?page=1",
"last": "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans?page=3",
"prev": null,
"next": "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 3,
"links": [],
"path": "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans",
"per_page": 1,
"to": 1,
"total": 3
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Start a check now
POST
/sites/{site}/scans
Token ability: write
Queues a check of the site with the plan's number of pages. One check by hand per site an hour (a month on Free), never while a check of the site is running: both are answered with 422. Follow the status with "Status of a check" or a webhook.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| site(required) | path | string |
The site's id.
|
Response
202 The check is queued.
application/json
{
"data": {
"id": "01j9z5c1v3b5n7m9q1w3e5r7t9",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"kind": "manual",
"status": "queued",
"pages_requested": 2000,
"pages_scanned": 0,
"issues": null,
"previous_issues": null,
"by_impact": null,
"error": null,
"queued_at": "2026-10-14T09:15:00+00:00",
"started_at": null,
"finished_at": null,
"links": {
"self": "https://pandus.online/api/v1/scans/01j9z5c1v3b5n7m9q1w3e5r7t9",
"report": null,
"pdf": null,
"exports": null,
"acr": null
}
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 422 | A check of the site is running, or one was started by hand too recently. | You started a check of this site less than an hour ago. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl -X POST "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->post('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.post(
"https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/scans",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Status of a check
GET
/scans/{scan}
Token ability: read
The status (queued, running, completed, failed), the number of pages and problems.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
Response
200 The check.
application/json
{
"data": {
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"kind": "scheduled",
"status": "completed",
"pages_requested": 2000,
"pages_scanned": 37,
"issues": 12,
"previous_issues": 15,
"by_impact": {
"critical": 2,
"serious": 7,
"moderate": 3
},
"error": null,
"queued_at": "2026-10-14T06:00:02+00:00",
"started_at": "2026-10-14T06:00:05+00:00",
"finished_at": "2026-10-14T06:03:41+00:00",
"links": {
"self": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8",
"report": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report",
"pdf": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf",
"exports": {
"json": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=json",
"csv": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv",
"github": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=github",
"jira": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=jira"
},
"acr": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr"
}
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Reports
Results of a completed check, as JSON, PDF, files for developers or a comparison.
Export the latest check of a site
GET
/sites/{site}/export
Token ability: read
The same files as "Export the issues for developers", for the latest completed check of the site (the one its report shows), so you do not have to look up the check first. The Pandus-Scan header names the check the file is from. format=pdf is the PDF report (every plan with the API); the other formats come with paid plans. The PDF is prepared synchronously on the first request for a language (up to a minute, then kept); keep the read timeout of your client at 120 seconds. 20 requests a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| site(required) | path | string |
The site's id.
|
| format | query | string |
json (default) a JSON document; csv one row per element (UTF-8 with BOM); github Markdown with one GitHub issue per rule; jira a CSV for Jira's importer with one issue per rule; pdf the PDF report for the site's owner. GitHub and Jira files list problems (WCAG A/AA) only; CSV and JSON add the recommendations.
Values: json, csv, github, jira, pdf Default: json |
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The file as an attachment: Content-Disposition carries the file name, Pandus-Scan the check it is from. JSON, CSV (UTF-8 with BOM), Markdown or PDF by format.
application/json
{
"format": "accessibility-issues/1",
"disclaimer": "Automated check, not a full audit. Not legal advice.",
"generated_by": "Nova Studio",
"site": {
"name": "Example Shop",
"url": "https://shop.example/"
},
"checked_at": "2026-10-14T06:03:41+00:00",
"pages_checked": 37,
"standard": "WCAG 2.2 A/AA, EN 301 549",
"engine": {
"axe": "4.10.0"
},
"language": "en",
"rules": [
{
"rule": "button-name",
"type": "problem",
"severity": "critical",
"title": "Some buttons have no name",
"summary": "Some buttons have no readable label, for example an icon-only \"add to cart\", search or menu button.",
"how_to_fix": "Give every button a name: visible text, aria-label, or aria-labelledby pointing to visible text.",
"platform_tip": "Usually icon buttons in the theme or buttons injected by apps.",
"wcag": [
"4.1.2 Name, Role, Value (A)"
],
"en301549": [
"9.4.1.2"
],
"count": 1,
"pages": 1,
"issues": [
{
"page": "https://shop.example/cart",
"selector": ".cart-icon",
"html": "<button class=\"cart-icon\"></button>",
"failure_summary": "Fix any of the following: Element does not have inner text that is visible to screen readers",
"is_new": false
}
]
}
]
}
text/csv
rule,type,severity,wcag,title,page_url,selector,html,failure_summary,new_since_previous_check
button-name,problem,critical,"4.1.2 Name, Role, Value (A)","Some buttons have no name",https://shop.example/cart,.cart-icon,"<button class=""cart-icon""></button>","Fix any of the following",no
text/markdown
# Accessibility issues of Example Shop
> Automated check, not a full audit. Not legal advice.
## [critical] Some buttons have no name (button-name)
- [ ] https://shop.example/cart — `.cart-icon`
Labels: `accessibility`, `a11y:critical`, `axe:button-name`
application/pdf
A file; no example shown.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | Without the ability read, without the API in the plan, or without exports in the plan (CSV, JSON, GitHub and Jira files; the PDF is in every plan with the API). |
Issue exports are not included in the current plan of this team. / Invalid ability provided. |
| 404 | The site is not in this team, or it has no completed check yet. | The site has no completed check yet. / Not found. |
| 422 | The request is not valid or a plan limit stops it. | The url field is required. (and 1 more error) |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
| 503 | The PDF could not be prepared right now; try again after Retry-After seconds. |
The PDF could not be prepared right now. Please try again in a minute. |
Code samples
curl
curl "https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/export?format=pdf&lang=de" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
--fail -o report.pdf
JavaScript
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/export?format=pdf&lang=de', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
await writeFile('report.pdf', Buffer.from(await response.arrayBuffer()));
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->get('https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/export', [
'format' => 'pdf',
'lang' => 'de',
])
->throw();
file_put_contents('report.pdf', $response->body());
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/sites/01j9z3m2c5v7b9n1m3k5j7h9g1/export",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}"},
params={
"format": "pdf",
"lang": "de",
},
timeout=30,
)
response.raise_for_status()
with open("report.pdf", "wb") as file:
file.write(response.content)
The report as JSON
GET
/scans/{scan}/report
Token ability: read
The owner report of a completed check, the same content as in the cabinet: problem groups with plain-language explanations and fixing tips, examples, affected pages, totals by severity, the comparison with the previous check and the trend. Recommendations (AAA, best practice) are under advisory. Screenshot URLs need the same token.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The report.
application/json
{
"data": {
"scan_id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"locale": "en",
"url": "https://shop.example/",
"title": "Example Shop",
"platform": "shopify",
"platform_name": "Shopify",
"checked_at": "2026-10-14T06:03:41.000000Z",
"screen": null,
"totals": {
"critical": 1,
"serious": 0,
"moderate": 0,
"minor": 0
},
"total": 1,
"groups": [
{
"rule": "button-name",
"severity": "critical",
"rank": 0,
"count": 1,
"new_count": 0,
"title": "Some buttons have no name",
"summary": "Some buttons have no readable label, for example an icon-only \"add to cart\", search or menu button.",
"impact": "Screen reader and voice control users cannot tell which button adds to cart, opens the menu or submits the search.",
"who": [
"blind customers",
"screen reader users",
"voice control users"
],
"how_to_fix": "Give every button a name: visible text, aria-label, or aria-labelledby pointing to visible text.",
"platform_tip": "Usually icon buttons in the theme or buttons injected by apps. Add labels in the theme's Liquid snippets.",
"machine": false,
"has_card": true,
"help": "Buttons must have discernible text",
"refs": [
"WCAG 2.2 4.1.2 (A) Name, Role, Value",
"EN 301 549 9.4.1.2"
],
"pages": [
{
"url": "https://shop.example/cart",
"count": 1
}
],
"examples": [
{
"id": 4182,
"page": "https://shop.example/cart",
"target": ".cart-icon",
"html": "<button class=\"cart-icon\"></button>",
"details": null,
"is_new": false,
"shot": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/evidence/issue/4182",
"shot_width": 1280,
"shot_height": 720
}
],
"boxes": [],
"n": 1
}
],
"advisory": [],
"pages": [
{
"url": "https://shop.example/cart",
"title": "Cart",
"issues": 1,
"error": null
}
],
"diff": {
"previous_id": "01j9yq3r5t7y9u1i3o5p7a9s1d",
"previous_date": "2026-10-07T06:02:57+00:00",
"previous_total": 2,
"new": 0,
"fixed": 1,
"remaining": 1,
"fixed_rules": [
{
"title": "Images have no text description",
"count": 1
}
]
},
"trend": [
{
"id": "01j9yq3r5t7y9u1i3o5p7a9s1d",
"date": "2026-10-07T06:02:57+00:00",
"total": 2
},
{
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"date": "2026-10-14T06:03:41+00:00",
"total": 1
}
]
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/report",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
The report as PDF
GET
/scans/{scan}/pdf
Token ability: read
The PDF report of a completed check, under the team's brand on plans with white-label. Synchronous: the first request for a language prepares the file (up to a minute; keep the read timeout of your client at 120 seconds) and the next ones get the stored file. If the renderer is busy or down the answer is 503 with Retry-After. The same file as GET /scans/{scan}/export?format=pdf. 20 requests a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The PDF file (Content-Disposition attachment).
application/pdf
A file; no example shown.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
| 503 | The PDF could not be prepared right now; try again after Retry-After seconds. |
The PDF could not be prepared right now. Please try again in a minute. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf?lang=de" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
--fail -o report.pdf
JavaScript
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf?lang=de', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
await writeFile('report.pdf', Buffer.from(await response.arrayBuffer()));
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf', [
'lang' => 'de',
])
->throw();
file_put_contents('report.pdf', $response->body());
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/pdf",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}"},
params={
"lang": "de",
},
timeout=30,
)
response.raise_for_status()
with open("report.pdf", "wb") as file:
file.write(response.content)
Export the issues for developers
GET
/scans/{scan}/export
Token ability: read
The issues of a completed check as a file for developers and ticket systems, the same files as the export menus in the cabinet. Included in paid plans (and for the check an express report was bought for). format=pdf returns the PDF report instead, as "The report as PDF" (every plan with the API). 20 requests a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| format | query | string |
json (default) a JSON document; csv one row per element (UTF-8 with BOM); github Markdown with one GitHub issue per rule; jira a CSV for Jira's importer with one issue per rule; pdf the PDF report for the site's owner. GitHub and Jira files list problems (WCAG A/AA) only; CSV and JSON add the recommendations.
Values: json, csv, github, jira, pdf Default: json |
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The file as an attachment: Content-Disposition carries the file name, Pandus-Scan the check it is from. JSON, CSV (UTF-8 with BOM), Markdown or PDF by format.
application/json
{
"format": "accessibility-issues/1",
"disclaimer": "Automated check, not a full audit. Not legal advice.",
"generated_by": "Nova Studio",
"site": {
"name": "Example Shop",
"url": "https://shop.example/"
},
"checked_at": "2026-10-14T06:03:41+00:00",
"pages_checked": 37,
"standard": "WCAG 2.2 A/AA, EN 301 549",
"engine": {
"axe": "4.10.0"
},
"language": "en",
"rules": [
{
"rule": "button-name",
"type": "problem",
"severity": "critical",
"title": "Some buttons have no name",
"summary": "Some buttons have no readable label, for example an icon-only \"add to cart\", search or menu button.",
"how_to_fix": "Give every button a name: visible text, aria-label, or aria-labelledby pointing to visible text.",
"platform_tip": "Usually icon buttons in the theme or buttons injected by apps.",
"wcag": [
"4.1.2 Name, Role, Value (A)"
],
"en301549": [
"9.4.1.2"
],
"count": 1,
"pages": 1,
"issues": [
{
"page": "https://shop.example/cart",
"selector": ".cart-icon",
"html": "<button class=\"cart-icon\"></button>",
"failure_summary": "Fix any of the following: Element does not have inner text that is visible to screen readers",
"is_new": false
}
]
}
]
}
text/csv
rule,type,severity,wcag,title,page_url,selector,html,failure_summary,new_since_previous_check
button-name,problem,critical,"4.1.2 Name, Role, Value (A)","Some buttons have no name",https://shop.example/cart,.cart-icon,"<button class=""cart-icon""></button>","Fix any of the following",no
text/markdown
# Accessibility issues of Example Shop
> Automated check, not a full audit. Not legal advice.
## [critical] Some buttons have no name (button-name)
- [ ] https://shop.example/cart — `.cart-icon`
Labels: `accessibility`, `a11y:critical`, `axe:button-name`
application/pdf
A file; no example shown.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | Without the ability read, without the API in the plan, or without exports in the plan (CSV, JSON, GitHub and Jira files; the PDF is in every plan with the API). |
Issue exports are not included in the current plan of this team. / Invalid ability provided. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 422 | The request is not valid or a plan limit stops it. | The url field is required. (and 1 more error) |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
| 503 | The PDF could not be prepared right now; try again after Retry-After seconds. |
The PDF could not be prepared right now. Please try again in a minute. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
--fail -o issues.csv
JavaScript
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export?format=csv', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
await writeFile('issues.csv', Buffer.from(await response.arrayBuffer()));
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export', [
'format' => 'csv',
])
->throw();
file_put_contents('issues.csv', $response->body());
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/export",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}"},
params={
"format": "csv",
},
timeout=30,
)
response.raise_for_status()
with open("issues.csv", "wb") as file:
file.write(response.content)
Compare two checks
GET
/scans/{scan}/compare/{other}
Token ability: read
Before and after between two completed checks of one site, per rule: which problems (WCAG A/AA) were fixed, which are new and which are still open. Problems are matched by rule, page and element. Up to 25 elements are listed per rule and state; count has the full number.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| other(required) | path | string | ID of another completed check of the same site. The older of the two is "before". |
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The comparison.
application/json
{
"data": {
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"locale": "en",
"before": {
"id": "01j9yq3r5t7y9u1i3o5p7a9s1d",
"date": "2026-10-07T06:02:57+00:00",
"issues": 3
},
"after": {
"id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"date": "2026-10-14T06:03:41+00:00",
"issues": 2
},
"totals": {
"fixed": 2,
"new": 1,
"open": 1,
"before": 3,
"after": 2
},
"rules": [
{
"rule": "button-name",
"title": "Some buttons have no name",
"severity": "critical",
"fixed": {
"count": 1,
"items": [
{
"page": "https://shop.example/cart",
"selector": ".wishlist"
}
]
},
"new": {
"count": 0,
"items": []
},
"open": {
"count": 1,
"items": [
{
"page": "https://shop.example/cart",
"selector": ".cart-icon"
}
]
}
},
{
"rule": "color-contrast",
"title": "Text is hard to read because of low contrast",
"severity": "serious",
"fixed": {
"count": 0,
"items": []
},
"new": {
"count": 1,
"items": [
{
"page": "https://shop.example/cart",
"selector": ".price"
}
]
},
"open": {
"count": 0,
"items": []
}
}
]
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 422 | The checks belong to different sites, or it is the same check twice. | Both checks must belong to the same site. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/compare/01j9yq3r5t7y9u1i3o5p7a9s1d" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/compare/01j9yq3r5t7y9u1i3o5p7a9s1d', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/compare/01j9yq3r5t7y9u1i3o5p7a9s1d')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/compare/01j9yq3r5t7y9u1i3o5p7a9s1d",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
A screenshot from the report
GET
/scans/{scan}/evidence/{kind}/{id}
Token ability: read
The JPEG behind a shot or screen.url link of the JSON report.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| kind(required) | path | string |
page for a page screenshot, issue for the screenshot of one element.
Values: page, issue |
| id(required) | path | integer |
The number from the report (examples[].id for an element).
|
Response
200 The screenshot.
image/jpeg
A file; no example shown.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | The token lacks the ability for this request, the team's plan does not include the API, or the team is blocked. | Invalid ability provided. / The API is not included in the current plan of this team. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/evidence/issue/4182" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
--fail -o screenshot.jpg
JavaScript
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/evidence/issue/4182', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
await writeFile('screenshot.jpg', Buffer.from(await response.arrayBuffer()));
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/evidence/issue/4182')
->throw();
file_put_contents('screenshot.jpg', $response->body());
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/evidence/issue/4182",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
with open("screenshot.jpg", "wb") as file:
file.write(response.content)
The ACR draft as JSON
GET
/scans/{scan}/acr
Token ability: read
Draft of an Accessibility Conformance Report (VPAT 2.x terms) from a completed check: one entry per WCAG 2.2 A/AA criterion with the automated result and what a person entered. Agency plans. Only automated comes from the check (fails, no_failures, not_testable); conformance and remarks are only ever what a person sent, and "no automated failures" never becomes "supports". It is a draft, not a VPAT/ACR, until a person has evaluated every criterion (notice).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The draft.
application/json
{
"data": {
"scan_id": "01j9z4a7d2f4g6h8j0k2l4z6x8",
"site_id": "01j9z3m2c5v7b9n1m3k5j7h9g1",
"locale": "en",
"notice": "Draft based on an automated check. Not a VPAT/ACR until a person has evaluated every criterion. Not legal advice.",
"generated_by": "Nova Studio",
"site": {
"name": "Example Shop",
"url": "https://shop.example/"
},
"checked_at": "2026-10-14T06:03:41+00:00",
"pages_checked": 37,
"standard": "WCAG 2.2 A/AA",
"engine": {
"axe": "4.10.0"
},
"counts": {
"fails": 6,
"no_failures": 17,
"not_testable": 32
},
"filled": 1,
"levels": [
"supports",
"partially_supports",
"does_not_support",
"not_applicable",
"not_evaluated"
],
"criteria": [
{
"criterion": "1.1.1",
"name": "Non-text Content",
"level": "A",
"url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-content",
"automated": "fails",
"failures": [
{
"rule": "image-alt",
"title": "Images without a text alternative",
"count": 4,
"pages": 2
}
],
"conformance": "partially_supports",
"remarks": "Product photos in the gallery have no alt text; fixed in the next release."
},
{
"criterion": "1.2.1",
"name": "Audio-only and Video-only (Prerecorded)",
"level": "A",
"url": "https://www.w3.org/WAI/WCAG22/Understanding/audio-only-and-video-only-prerecorded",
"automated": "not_testable",
"failures": [],
"conformance": null,
"remarks": null
}
],
"unmapped": [],
"links": {
"html": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export?format=html&lang=en",
"pdf": "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export?format=pdf&lang=en"
}
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | Without the ability the request needs, without the API in the plan, or without ACR drafts in the plan (Agency plans). | ACR drafts are not included in the current plan of this team. / Invalid ability provided. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json"
JavaScript
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr')
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Fill in one ACR criterion
PUT
/scans/{scan}/acr/{criterion}
Token ability: write
Saves what a person decided for one criterion: the conformance level (VPAT 2.x terms, lower case as in levels) and remarks. A field left out keeps its value, null clears it; both empty removes the entry. Agency plans, ability write. Values are never set by Pandus itself: only what you send is stored. 60 requests a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| criterion(required) | path | string | Number of a WCAG 2.2 A/AA success criterion. 4.1.1 Parsing is not part of WCAG 2.2. |
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Request body application/json
| Field | Type | Description |
|---|---|---|
| conformance | string | null |
Values: supports, partially_supports, does_not_support, not_applicable, not_evaluated |
| remarks | string | null (max. 5000) |
Example
{
"conformance": "partially_supports",
"remarks": "Product photos in the gallery have no alt text; fixed in the next release."
}
Response
200 The criterion as saved.
application/json
{
"data": {
"criterion": "1.1.1",
"name": "Non-text Content",
"level": "A",
"url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-content",
"automated": "fails",
"failures": [
{
"rule": "image-alt",
"title": "Images without a text alternative",
"count": 4,
"pages": 2
}
],
"conformance": "partially_supports",
"remarks": "Product photos in the gallery have no alt text; fixed in the next release."
}
}
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | Without the ability the request needs, without the API in the plan, or without ACR drafts in the plan (Agency plans). | ACR drafts are not included in the current plan of this team. / Invalid ability provided. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 422 | An unknown criterion, a level that is not one of levels, remarks over 5000 characters, or an empty body. |
The selected conformance is invalid. |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
Code samples
curl
curl -X PUT "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/1.1.1" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"conformance": "partially_supports",
"remarks": "Product photos in the gallery have no alt text; fixed in the next release."
}'
JavaScript
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/1.1.1', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
conformance: 'partially_supports',
remarks: 'Product photos in the gallery have no alt text; fixed in the next release.',
}),
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
const { data } = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->acceptJson()
->put('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/1.1.1', [
'conformance' => 'partially_supports',
'remarks' => 'Product photos in the gallery have no alt text; fixed in the next release.',
])
->throw();
$data = $response->json('data');
Python
import os
import requests
response = requests.put(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/1.1.1",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}", "Accept": "application/json"},
json={
"conformance": "partially_supports",
"remarks": "Product photos in the gallery have no alt text; fixed in the next release.",
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
Download the ACR draft
GET
/scans/{scan}/acr/export
Token ability: read
The ACR draft as a PDF or an HTML file, under the team's brand on plans with white-label, the same file as in the cabinet. The draft wording is at the start, at the end and, in the PDF, in the footer of every page. Made on every request (remarks change), synchronously: keep the read timeout of your client at 120 seconds. Agency plans. 20 requests a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| scan(required) | path | string |
The check's id.
|
| format | query | string |
pdf (default) or html, a single file that opens in any browser.
Values: pdf, html Default: pdf |
| lang | query | string |
Report language; the site's report language when left out.
Values: en, uk, de, pl, cs, ro, ru |
Response
200 The file (Content-Disposition attachment, e.g. acr-draft-shop.example-2026-10-14.pdf).
application/pdf
A file; no example shown.
text/html
A file; no example shown.
| Status | When | message |
|---|---|---|
| 401 | No token, an unknown token or a deleted one. | Unauthenticated. |
| 403 | Without the ability the request needs, without the API in the plan, or without ACR drafts in the plan (Agency plans). | ACR drafts are not included in the current plan of this team. / Invalid ability provided. |
| 404 | Not found in this team, or older than the plan's history. | Not found. |
| 409 | The check is still queued or running, or it failed. | The scan is not completed yet. |
| 422 | The request is not valid or a plan limit stops it. | The url field is required. (and 1 more error) |
| 429 | Too many requests in a minute. Wait Retry-After seconds. |
Too Many Attempts. |
| 503 | The PDF could not be prepared right now; try again after Retry-After seconds. |
The PDF could not be prepared right now. Please try again in a minute. |
Code samples
curl
curl "https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export?format=pdf&lang=en" \
-H "Authorization: Bearer $PANDUS_TOKEN" \
--fail -o acr-draft.pdf
JavaScript
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export?format=pdf&lang=en', {
headers: {
Authorization: `Bearer ${process.env.PANDUS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Pandus API: HTTP ${response.status}`);
await writeFile('acr-draft.pdf', Buffer.from(await response.arrayBuffer()));
PHP
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('PANDUS_TOKEN'))
->get('https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export', [
'format' => 'pdf',
'lang' => 'en',
])
->throw();
file_put_contents('acr-draft.pdf', $response->body());
Python
import os
import requests
response = requests.get(
"https://pandus.online/api/v1/scans/01j9z4a7d2f4g6h8j0k2l4z6x8/acr/export",
headers={"Authorization": f"Bearer {os.environ['PANDUS_TOKEN']}"},
params={
"format": "pdf",
"lang": "en",
},
timeout=30,
)
response.raise_for_status()
with open("acr-draft.pdf", "wb") as file:
file.write(response.content)