---
title: "FREE Remote Preview terms acceptance (API)"
manual: "Accessibility Quality Gate"
version: "2.1"
permalink: "https://docs.typo3.org/permalink/priebera/typo3-a11y-quality-gate:developer-free-acceptance-api@2.1"
source: "Developer/FreeRemotePreviewAcceptanceApi.rst"
rendered: "2026-10-27T10:03:44+00:00"
---

# FREE Remote Preview terms acceptance (API) {#developer-free-acceptance-api}

Since 2.1.0 the extension activates the FREE Remote Preview only with an
acceptance of the FREE terms and DPA that the AQG service has recorded
([Activating the FREE Remote Preview](https://docs.typo3.org/permalink/priebera/typo3-a11y-quality-gate:usage-remote-scans-free-activation@2.1)). This page is the contract the
extension implements; the AQG service must implement and enforce the same. The
extension fails closed: while the endpoints answer `404` or are unreachable,
activation is impossible and FREE scans stay off.

All requests go to the AQG API base URL (`A11Y_QUALITY_GATE_PRO_API_BASE_URL`
or the default), `Accept: application/json`, JSON bodies. Errors use the
service's usual body \``{"success": false, "error": {"code": "…", "message":
"…", "status": 409, "details": {…}}}`\`.

## GET /free-preview/terms?language=en|de {#developer-free-acceptance-api-terms}

The terms in force. Public, cacheable (the extension caches a complete answer
for one hour).

```json
{
  "success": true,
  "terms": {
    "version": "free-2026-11",
    "effectiveAt": "2026-11-01T00:00:00Z",
    "documents": [
      {"type": "free_terms", "language": "en", "title": "AQG FREE Remote Preview Terms",
       "version": "1.0", "url": "https://typo3.priebera.sk/en/legal/archive/<sha256>", "sha256": "<64 hex>"},
      {"type": "dpa", "language": "en", "title": "AQG FREE Remote Preview Data Processing Terms",
       "version": "1.0", "url": "https://typo3.priebera.sk/en/legal/archive/<sha256>", "sha256": "<64 hex>"}
    ],
    "statements": {
      "en": {"sha256": "<sha256 of the English declaration>"},
      "de": {"sha256": "<sha256 of the German declaration>"}
    }
  }
}
```

-   `version` and document `version`: `[A-Za-z0-9._:-]{1,64}`.
-   Documents: `type` `free_terms` and `dpa` are both required, `language`
    `en`/`de`, `url` must be `https`, `sha256` is the hash of the
    published file. Anything else makes the extension treat the terms as not
    published.
-   `statements`: the SHA-256 (UTF-8, exact text) of the declaration the
    administrator ticks. The extension ships it verbatim in
    `Classes/FreePreview/FreePreviewStatement.php` (`VERSION` =
    `free-statement-2026-10-v2`: one combined confirmation that names the
    FREE Remote Preview Terms and the FREE Remote Preview Data Processing
    Terms with their versions and the authority to act for the organisation)
    and does not offer activation when its hash is not listed.
-   `404` = not published yet.

## POST /free-preview/acceptances {#developer-free-acceptance-api-accept}

Records an acceptance. Request:

```json
{
  "installationId": "<anonymous installation id>",
  "siteUrl": "https://www.example.org/",
  "siteIdentifier": "main",
  "domain": "example.org",
  "termsVersion": "free-2026-11",
  "documents": [
    {"type": "free_terms", "language": "en", "version": "1.0", "sha256": "<64 hex>"},
    {"type": "dpa", "language": "en", "version": "1.0", "sha256": "<64 hex>"}
  ],
  "statement": {"version": "free-statement-2026-10-v2", "language": "en",
                "text": "I accept the AQG FREE Remote Preview Terms, version 1.0, …", "sha256": "<64 hex>"},
  "organisation": {"name": "Example GmbH"},
  "acceptedBy": {"name": "Ada Admin", "email": "ada@example.org", "typo3UserId": 7,
                 "role": "typo3_administrator", "authorityConfirmed": true},
  "idempotencyKey": "<64 hex>",
  "clientTimestamp": "2026-10-20T10:15:29Z",
  "extension": {"version": "2.1.0", "typo3Version": "14.3.9"}
}
```

Response `201` (`200` for a replay of the same `idempotencyKey`):

```json
{
  "success": true,
  "acceptance": {
    "id": "acc_01J…",
    "status": "valid",
    "termsVersion": "free-2026-11",
    "installationId": "<as sent>",
    "domain": "example.org",
    "acceptedAt": "2026-10-20T10:15:30.000Z",
    "validUntil": null,
    "replayed": false
  }
}
```

The extension stores the acceptance only when `id` matches
`[A-Za-z0-9._:-]{8,128}`, `status` is `valid`, `termsVersion`,
`installationId` and `domain` equal what was sent and `acceptedAt` (and
`validUntil` when set) is ISO 8601 with a time zone.

Errors the extension distinguishes:

| Code | Status | Meaning |
| --- | --- | --- |
| `validation_failed` | 400 | Missing or malformed field |
| `invalid_installation_proof`, | 403 | The site does not serve this installation's |
| `installation_proof_*` |  | proof (see below) |
| `free_domain_mismatch` | 403 | `domain` is not the host of `siteUrl` |
| `free_terms_outdated` | 409 | `termsVersion` or a document hash is not the version in force (`details.currentVersion`) |
| `free_terms_statement_mismatch` | 409 | `statement.sha256` is not published for the version, or does not match `statement.text` |
| `idempotency_key_reused` | 409 | Same key, different payload |
| `rate_limit_exceeded` | 429 | Throttled |

## POST /free-preview/acceptances/status {#developer-free-acceptance-api-status}

Request `{"installationId", "siteUrl", "siteIdentifier", "acceptanceId"}`.
Response \``{"success": true, "acceptance": {"id", "status": "valid|outdated|revoked|expired",
"validUntil"}, "currentTermsVersion": "free-2026-11"}`\`. The extension changes
its local state only for an answer with the same `id`. `404`
`free_terms_acceptance_not_found` and `403` `free_terms_acceptance_mismatch`
mark the local acceptance as no longer accepted.

## Enforcement on FREE token and submit {#developer-free-acceptance-api-enforcement}

The extension sends `freeTermsAcceptanceId` with the FREE `POST /auth/token`
(`installationId` flavour) and with the FREE `POST /crawl/submit`. The
service must refuse FREE access with `403` and one of these codes, which the
extension records and explains:

-   `free_terms_acceptance_required` — no acceptance id, or unknown;
-   `free_terms_outdated` — accepted terms are not the version in force;
-   `free_terms_acceptance_revoked`, `free_terms_acceptance_expired`;
-   `free_terms_acceptance_mismatch` — the acceptance belongs to another
    installation or domain.

## Requirements for the AQG service {#developer-free-acceptance-api-backend}

-   **Trusted time**: `acceptedAt` is generated by the service in UTC when the
    record is stored. `clientTimestamp` is stored for information only and
    never used as the acceptance time.
-   **Immutable records**: an acceptance is never updated in place. Revocation,
    expiry and supersession are separate events or status columns with their own
    timestamps; switching FREE off in TYPO3 does not touch the record.
-   **Identity**: verify that the site serves this installation, as for the FREE
    token (`GET <site base path>/_aqg/free-preview-proof` →
    \``{"version": 1, "proof": base64url(sha256("aqg-free-preview:v1n" +
    installationId + "n" + normalizedHost))}`\`). Bind the record to
    `installationId` \+ normalised `domain`; another installation or domain
    never inherits it.
-   **Domain form**: `domain` and `normalizedHost` are the host in its ASCII
    form as WHATWG URL parsing derives it (UTS #46, non-transitional), lower
    case, without a leading `www.` and trailing dots. TYPO3 keeps an
    internationalised site base in Unicode; the extension sends `müller.de` as
    `xn--mller-kva.de` and accepts a confirmation only for that form. A
    Unicode host that cannot be converted has no domain and is never activated.
-   **Versions**: accept only the `termsVersion` in force, the published
    document hashes for it and a published `statement.sha256` that equals the
    SHA-256 of `statement.text`.
-   **Idempotency**: `(installationId, domain, idempotencyKey)`; a replay with
    the same payload returns the existing record (`replayed: true`) while that
    record is still `valid`, a different payload \``409
    idempotency_key_reused`\`. When the matching record has been revoked or has
    expired, a new submission records a new acceptance (with a new id and
    `acceptedAt`) instead of replaying the old one. The extension derives the
    key from installation, domain, terms version, statement hash, organisation,
    administrator name and email, so retries and double clicks never create a
    second record.
-   **Evidence stored**: acceptance id, SHA-256 fingerprint of the installation id, domain, site URL, site
    identifier, terms version, document types/languages/versions/hashes,
    statement version/language/hash/text, organisation name, administrator name,
    email and TYPO3 user id, authority declaration, server `acceptedAt`,
    request id, extension and TYPO3 version, `clientTimestamp`. The client IP is kept apart, in the service's
    45-day request log for abuse handling. No passwords, tokens or page content.
-   **Enforcement**: check the acceptance on every FREE token and submit, as
    above; the TYPO3 UI is not a security boundary.
-   **Retention of FREE data** (owner direction, to be confirmed legally and
    implemented on the service, not in TYPO3): transient HTML/DOM discarded after
    processing (at most 24 hours where technically needed), FREE scan results at
    most 30 days, FREE technical and security logs at most 45 days, no FREE
    screenshots. Acceptance records follow the contractual evidence retention.
