Resources
The API
Run analyses from your own tools, from a single page to the whole site
Contents
Get started
The API answers under https://api.getgrammage.com/v1, in JSON, and its full contract can be read in openapi.json, which you can open in any OpenAPI-compatible tool or give to a client generator. It analyses a single page as well as the whole site, but never the source code, which is only read in your CI.
A measurement often takes longer than an HTTP request can wait, so everything that measures is asynchronous. The request answers right away with a 202 and the address to follow, which you then read again or listen to live until the verdict.
Authentication
The free test, the attestations and the badge are public, with no account or key. The attestation and result routes require your licence, sent in the Authorization header, and a licence only ever reads back its own results.
curl https://api.getgrammage.com/v1/results/<id> \
-H "Authorization: Bearer $GRAMMAGE_LICENSE"The routes
| Route | Access | Role |
|---|---|---|
POST /v1/tests | public | queues a page for the free test |
GET /v1/tests/:id | public | the state of the test, then its full report |
GET /v1/tests/:id/events | public | the same state live, as Server-Sent Events |
GET /v1/tests/:id/brief | public | the brief for a coding agent, or the RGESN helper, in Markdown |
POST /v1/tests/:id/cancel | public | cancels the test, which still counts towards the limits of the day |
POST /v1/certifications | licence with badge | has a domain measured and verified by Solyzon, without a CI |
POST /v1/results | licence | records a report signed by your CI, which is what grammage certify does |
GET /v1/results/:id | the licence that sent it | the result, its verification and its detailed report |
GET /v1/results/:id/events | the licence that sent it | the progress of the verification live |
GET /v1/attestations/:host | public | the current attestation of the site, or of a page with ?path= |
GET /v1/attestations/:host/documents | public | the verified documents and their SHA-256 fingerprint |
GET /v1/badge.js | public | the badge module |
GET /v1/openapi.json | public | the OpenAPI contract of the API |
GET /v1/version | public | the version of the API and of the engine, and the one of the report format |
The free test
Anyone can have a page measured, with no licence or account, three times a day per domain. The page is measured in a single pass, on mobile and in the auto category by default, with the full report, its fixes and the gain of each, but no attestation is signed, since it is a test.
curl -s https://api.getgrammage.com/v1/tests \
-H 'content-type: application/json' \
-d '{"url": "https://example.com/", "device": "mobile", "category": "auto"}'const response = await fetch('https://api.getgrammage.com/v1/tests', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com/' }),
});
const { id, url, status, queuePosition } = await response.json();import requests
response = requests.post('https://api.getgrammage.com/v1/tests', json={'url': 'https://example.com/'})
test = response.json()
print(test['id'], test['status'], test['queuePosition'])The response arrives as a 202 with the identifier of the test and its place in the queue. If the same page was already measured less than four hours ago, the API returns that result as a 200, without counting towards your limits.
{
"id": "6f1c0e3a-8b1d-4c52-9a7e-2d4b0f3c9e11",
"url": "https://api.getgrammage.com/v1/tests/6f1c0e3a-8b1d-4c52-9a7e-2d4b0f3c9e11",
"status": "pending",
"queuePosition": 2
}Follow a measurement
A measurement is followed in two ways, by listening to its event stream, which sends the state on every change then closes at the verdict, or by reading GET /v1/tests/:id again until status leaves pending. The stream avoids reading again for nothing, so it is the one we prefer.
const events = new EventSource('https://api.getgrammage.com/v1/tests/' + id + '/events');
events.onmessage = (event) => {
const { status, queuePosition, progress } = JSON.parse(event.data);
if (status !== 'pending') {
events.close();
}
};let test;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
test = await (await fetch('https://api.getgrammage.com/v1/tests/' + id)).json();
} while (test.status === 'pending');
const score = test.report?.pages[0]?.score.score;{
"status": "pending",
"queuePosition": null,
"progress": {
"step": "measure",
"page": 1,
"pages": 1,
"run": 1,
"runs": 1,
"url": "https://example.com/",
"updatedAt": "2026-09-30T09:30:12.000Z"
},
"detail": null
}step is discovery while a site is being discovered, measure while a page is being measured and report once the pages are measured, and progress stays null while the measurement waits for its turn, queuePosition then giving its place. The final state is passed, with the full report in report, or failed and error, with a failure block that says why, or cancelled when the test was cancelled.
A test that is no longer needed is cancelled with POST /v1/tests/:id/cancel. It then leaves the queue, or its result is ignored if the measurement had already started, but it still counts towards the limits of the day, so that they cannot be bypassed by cancelling. Cancelling twice answers the same, and a test that is already finished answers 409.
The brief and the RGESN helper
Once the page is measured, the same measurement gives the fixing brief for a coding agent and the helper for the eco-design declaration, both in Markdown.
curl -s 'https://api.getgrammage.com/v1/tests/<id>/brief?format=agent' > grammage-agent.md
curl -s 'https://api.getgrammage.com/v1/tests/<id>/brief?format=rgesn' > grammage-rgesn.mdAttested results
A result is born in two ways, from a signed report that your CI sends with POST /v1/results, which grammage certify does for you, or from an on-demand verification with POST /v1/certifications. In both cases, the response gives its identifier, and GET /v1/results/:id then returns its state, its place in the queue, the progress of the Solyzon measurement under verification.progress and, once verified, its detailed report.
Its event stream requires the licence, and since EventSource cannot set an Authorization header, you read it with fetch and its streamed body, or you relay it from your own server. A CI that sends the same report again with the same Idempotency-Key header gets its result back without creating a second one.
curl -N https://api.getgrammage.com/v1/results/<id>/events \
-H "Authorization: Bearer $GRAMMAGE_LICENSE"const response = await fetch('https://api.getgrammage.com/v1/results/' + id + '/events', {
headers: { authorization: 'Bearer ' + license },
});
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
for (const line of chunk.value.split('\n')) {
if (line.startsWith('data: ')) {
console.log(JSON.parse(line.slice(6)));
}
}
}Attestations
The current attestation of a domain is public, it is the one the badge reads. It arrives signed, and the public key that verifies it is resultats-2026-09. The documents route gives the three verified documents, the report in HTML and PDF and the attestation in PDF, with their SHA-256 fingerprint, the same ones that the verification page publishes.
curl -s https://api.getgrammage.com/v1/attestations/example.com
curl -s 'https://api.getgrammage.com/v1/attestations/example.com?path=/tarifs'
curl -s https://api.getgrammage.com/v1/attestations/example.com/documentsA badge placed on www. finds the attestation measured without it, and the other way round. These responses stay for one minute in the cache at Cloudflare and five minutes in the browser, so a new result is visible at most six minutes after it is recorded.
Read the report
The report is the same everywhere, whether the measurement comes from the API, from your CI or from a machine. Its shape never loses a field as long as schemaVersion is 1, and a change of that kind would move the schema to 2.
| Field | What it says |
|---|---|
pages[].score.score | the score out of 100 of the page |
pages[].score.context | the category used, inferred when Grammage deduced it |
pages[].co2.swd4 | the carbon per view, according to the SWD v4 model |
pages[].stack | the recognised framework, which chooses the specific fixes |
pages[].advice | the fixes, sorted by weight then by gain, with fix, stackFix and gain |
pages[].valid | false when the page could not be measured, with its reason |
schemaVersion | the shape of the report, which loses no field as long as it is 1 |
tool.version, protocol, score.method | the three versions, without which two scores cannot be compared |
Errors
Errors arrive in the application/problem+json format of RFC 9457, with a title and a detail in French that you can display as they are to your users.
{
"type": "about:blank",
"title": "Trop de demandes",
"status": 429,
"detail": "au plus 3 tests gratuits par jour et par domaine"
}| Code | What it means |
|---|---|
200 | the response, or for a test, the same page already measured less than 4 hours ago |
202 | the request is queued, its address is in url and in the Location header |
400 | the request cannot be read, detail says which field to fix |
401 | the licence is missing or invalid |
403 | the licence is no longer active, or does not cover this domain or this feature |
404 | the identifier or the domain is unknown |
422 | the report targets an address that is not the one of the attested domain |
429 | a limit is reached, Retry-After says when to try again |
503 | the queue is full, try again after Retry-After |
When a measurement fails
A measurement that fails does not return an HTTP error, since the request itself was processed. It goes to failed or error with a failure block, whose code says what happened and retryable whether it is worth offering a new attempt. Passing network errors are already retried once on our side before getting there.
{
"code": "bot-challenge",
"message": "le site bloque les navigateurs automatisés par Cloudflare, il faut autoriser l’agent Grammage",
"retryable": false,
"vendor": "Cloudflare",
"httpStatus": 403
}| Code | What happened | Retry |
|---|---|---|
dns | the domain name does not answer | yes |
unreachable | the site refuses or cuts the connection | yes |
timeout | the site takes too long to answer | yes |
tls | the HTTPS certificate is invalid or expired | no |
http-status | the page answers 4xx or 5xx, httpStatus gives the detail | on 5xx and 429 |
bot-challenge | an anti-robot service blocks the measurement, vendor names it | no |
not-html | the address does not return an HTML page | no |
unresponsive | the page stops answering during the measurement | yes |
private-address | the address leads to a private network | no |
measurement-error | the measurement did not complete on our side | yes |
Limits
The limits protect the machines that measure, and each one answers 429 with a Retry-After header, without ever charging anything.
| What | Limit |
|---|---|
| Free test | 3 per day per domain, 10 per day per address, 1 page |
| Same page already tested | the result under 4 hours old is returned, without counting towards the limits |
| Event streams | 30 minutes at most, 5 open at a time per address |
| CI verification | 10 per day per domain, 60 per hour and 300 per day per licence |
| On-demand verification | 1 per week per domain, within the monthly quota of the licence |
| Pages of a verification | 100 at most |
| Public reads | one limit per minute per address, beyond which 429 |
What the API keeps, and for how long
Every night, a purge applies these durations, and the latest result of each site or page always escapes the first two. Public calls, including those of the badge, keep no IP address and are only counted per minute, per route and per status.
| Data | Duration | What is erased |
|---|---|---|
| Free test | 30 days | the result, its measurement and its report are erased |
| Full report | 13 months | the report is erased, the result remains |
| Result and attestation | 6 years | both are erased |
| IP address of a call under licence | 12 months | the address is erased |
| Request log | 12 months | the lines are erased |
See what your site really weighs
Grammage brings together in a single tool the audit in a real browser, the code analysis in the CI/CD and a signed result that anyone can verify.
Free trial, no commitment.
Try it for free