# MammothWP Acceptance Logs: use cases

## Usage

Users in the selected roles are prompted the next time they load the site or the dashboard while logged in. Whoever activated the plugin becomes its first manager; managers are the only people who can read the acceptance log, under Users > Terms acceptance log.

## Download and install

The wordpress.org listing is pending review. Until it is live, install from
this repository, as described on
<https://mammothwp.com/plugins/mammothwp-acceptance-logs/>:

1. Download the ZIP of `main` from GitHub (**Code → Download ZIP**).
2. Unzip it, rename the folder `admin-terms-acceptance-main` to
   `mammoth-acceptance-logs`, and zip that folder again.
3. In WordPress go to **Plugins → Add New → Upload Plugin**, choose the zip and
   install it.
4. **Activate** the plugin, then open **Settings → Terms & Conditions**.

The GitHub tree has no `vendor/` directory. The core acceptance flow works
without it, but the REST API, WP-CLI, the Abilities API, the Support screen and
PDF receipts need the dependencies. Run this inside the plugin folder before
zipping (or on the server):

```bash
composer install --no-dev --optimize-autoloader
```

Requires WordPress 6.2+ and PHP 8.1+. To build a distributable zip from a
checkout, use `composer build` (see Contributing).

Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/README.md

## Usage (README.md)

Full details in [`docs/usage.md`](docs/usage.md).

| Surface | Example |
|---|---|
| Admin | Settings → Terms & Conditions; Users → Terms acceptance log |
| REST | `GET /wp-json/mmwp/v1/terms/acceptances?version_id=3` (plugin managers) |
| WP-CLI | `wp mmwp terms publish --content="$(cat terms.html)" --only-if-changed` |
| Abilities | `mmwp-acceptance-logs/list-acceptances` (read-only) |

Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/README.md

## Support

Use the support forum on wordpress.org, or e-mail info@mammothwp.com. The
plugin never sends anything on its own; see [`docs/privacy.md`](docs/privacy.md).

Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/README.md

## Questions answered in the plugin readme

### Is this a cookie or consent plugin?

No, and the distinction drives the whole design. Accepting Terms & Conditions is an access-control and contractual-compliance mechanism resting on Art. 6(1)(b) and 6(1)(f) GDPR, not consent under Art. 6(1)(a). Practically: acceptance is not withdrawable in the Art. 7(3) sense, and records are not deleted on request. If you need cookie consent, you need a different plugin — run both.

### Which roles are prompted?

Until you choose, **every role**. The plugin does not guess a safe subset, and picking the roles is step 3 of the setup for that reason. Settings → Terms & Conditions lists every role on the site.

### Does it lock users out until they accept?

No. The popup has a "skip for now" button, and a user who clicks it keeps working. Skipping is not recorded as an acceptance or a decline: it only hides the popup for that user for 12 hours (filterable with `mmwp_terms_skip_duration`) and fires the `mmwp_terms_skipped` action; nothing is written to the acceptance log or audit events. What persists is a non-dismissable notice on every wp-admin screen, shown only to logged-in users in scope who have not accepted the current version, until they accept. It is a prompt with no exit, not a wall — a hard lockout risks stranding an administrator who needs the backend to fix the terms.

### What happens when I edit the terms?

Editing and publishing creates a **new version** with the next sequential number, archives the full text permanently, and re-prompts everyone in scope. Previous versions are never deleted, and republishing earlier text is a new publication with a new number rather than a silent return to an old one — so "revert the text and everyone counts as accepted again" cannot happen.
 Changing the text without publishing changes nothing. The daily cron only picks up changes made through a content source that bypasses the settings screen.

### Can a user withdraw their acceptance?

No. The record is evidence that a particular version was accepted at a particular moment; that fact does not stop being true. A user who does not want to accept can only skip the popup for now; that is not recorded (see above), and the acceptance prompt returns until they accept.

### How do I handle an erasure request?

Through WordPress's own Tools → Erase Personal Data — the plugin registers an eraser. It returns the acceptance records as **retained**, with the reasoning shown to the requester rather than applied silently, and erases the context (IP, User-Agent) of any record already past its retention period. Art. 17(3)(b) and (e) disapply the right to erasure where processing is necessary for legal claims, which is what proof of acceptance is. See the retention policy document in the plugin repository.

### How long is data kept?

Records live while the account exists, plus a dispute period defaulting to five years, after which the IP address, IP source and User-Agent are erased and the record is retained in anonymised form. Records are anonymised, never deleted. Published versions of the terms are never deleted at all — an acceptance whose text cannot be reproduced proves almost nothing.

### I'm an administrator. Why can't I see the acceptance log?

Access is an explicit list of user accounts, not a capability. That is deliberate: `manage_options` gets handed out liberally by role plugins and during onboarding, and the log is evidence about other people. Whoever activated the plugin is the first manager and can appoint others; the last manager cannot be removed.

### Can I delete a record, or fix a wrong one?

No, by design. There is no update or delete path in the code and no edit or delete affordance in the admin. If a record is wrong, the answer is a new record plus an audit event. Their absence is the control, not an oversight.

### Does it work behind Cloudflare or a load balancer?

Yes, but configure the trusted proxies first or the audit log records the proxy's address instead of the visitor's. Return your proxy's address or CIDR range from the `mmwp_terms_trusted_proxies` filter. Forwarded IP headers are ignored by default because trusting them unconditionally would let any client write its own address into the log.

### Does it support multisite?

Each site gets its own tables and settings, so activate it per site. Network activation is not supported: the tables are created for the site the activation runs on, not for every site in the network.

### Where do I get evidence for a dispute?

Users → Terms acceptance log, which shows every record with its integrity and delivery status, filterable, with CSV export and an on-demand integrity check. `the evidence-handling document in the plugin repository` covers the procedure, what the evidence actually shows, and the limitations you should disclose alongside it.

### What personal data is stored?

Per acceptance: user ID, login and e-mail as they were at the time, the version accepted, UTC and local timestamps with timezone, the site identifier, IP address (and how it was determined), User-Agent and cryptographic signatures. Three user-meta markers record the version each user accepted. No fingerprinting, MAC address or geolocation is collected.

### Does it support the WordPress personal data tools?

Yes. It registers a personal-data exporter and eraser (`wp_privacy_personal_data_exporters`, `wp_privacy_personal_data_erasers`) and adds suggested text to the Privacy Policy guide with `wp_add_privacy_policy_content()`.

### Does it contact any external service?

No. There are no remote calls, no content delivery network assets, no update checks and no telemetry. The bundled Support screen (Mammoth WP > Support) sends one e-mail through `wp_mail()` to info@mammothwp.com, only when an administrator presses Send. A site report is attached only if they tick "Include site report" after previewing exactly what it contains.

### What happens when I delete the plugin?

By default the acceptance evidence is kept. When you click Delete on the Plugins screen, a dialog asks whether to **keep the acceptance evidence (recommended)** or **delete it permanently**; deleting requires typing the plugin name. Either way the plugin's settings, scheduled tasks and options are removed. If you keep the evidence, the log, the version archive, the audit events, the per-user acceptance markers and the signing key stay in your database, so they can still be verified later.
 You can change the choice at any time under Settings > Terms & Conditions, or purge the evidence immediately with `wp mmwp terms purge` (it only reports what exists; add `--force` to delete, `--dry-run` to never delete). Developers can override the choice with the `mmwp_terms_drop_data_on_uninstall` filter.

### Can I manage it without the admin screens?

Yes: REST (`mmwp/v1/terms/...`), WP-CLI (`wp mmwp terms ...`) and the Abilities API, all declared once. Reading the acceptance log over any of them requires being a plugin manager and returns a redacted shape (no IP address, User-Agent or e-mail).

## Producing acceptance evidence for a dispute

What to do when someone has to demonstrate that a named person accepted a
named version of the terms — and what to be careful about while doing it.

### The procedure

1. **Verify the log before you export it.** Users → Terms acceptance log →
   *Verify integrity*. Do this first: an export taken from a log you have not
   checked tells you nothing about whether the log is intact. Record the
   result, including a clean one.
2. **Filter to the person.** Filter by user ID, or open their profile and
   follow "View this user's acceptance records".
3. **Export the CSV.** It carries every field, both signatures, the chain
   link, and a per-row verification verdict.
4. **Attach the version.** From Settings → Terms & Conditions → Version
   history, download the PDF of the version they accepted. The record names a
   version; the version supplies the text. One without the other proves half
   the point.
5. **State the integrity position honestly** — see below.

### What the evidence actually shows

| Element | What it establishes |
|---|---|
| User ID + username/email snapshot | Which account acted, readable even after the account is deleted |
| UTC timestamp + local time and offset | When, unambiguously, regardless of later timezone changes |
| Version ID, number, effective date, hash | Which document |
| Archived version text | What that document said — the part a hash alone cannot supply |
| Action | That the act was an acceptance |
| Site identifier | Which installation it happened on |
| IP address + source | Where the request came from, and whether that came from the connection or a trusted proxy header |
| User-Agent | Corroborating circumstances only — trivially spoofable, never primary proof |
| record_hash / prev_record_hash | That the record has not been altered or unlinked since it was written |

### Integrity: state it accurately

The signatures are **tamper-evidence, not tamper-proofing**. Overstating this
is worse than claiming nothing, because the overstatement is what gets
attacked.

- **Key in `wp-config.php`** (`MMWP_TERMS_INTEGRITY_KEY`): the key sits outside
  the database. Someone with SQL access alone can alter a record but cannot
  produce a matching signature, so the alteration is detectable.
- **Key in the database** (the default when the constant is absent): detects
  accidental corruption and careless direct edits. Someone who holds the
  database also holds the key and could re-sign an altered record. Say so.
- Neither protects against an attacker with full filesystem access, who could
  alter the plugin itself.

Set the constant *before* you need to rely on the log. Records signed under
the old key stop verifying when the key changes.

### Known limitations to disclose

Disclose these rather than let them be discovered. A limitation you volunteered
costs you far less than one found in cross-examination.

- **Context signature on anonymised records.** Once a record passes its
  retention period, the IP address and User-Agent are erased and the context
  signature is cleared with them. This is the retention policy working as
  documented, not tampering; the record's own signature still verifies. The
  `anonymised_at_utc` field carries the date, and an `anonymised` audit event
  carries the reason.
- **Chain gaps.** If two acceptances are recorded at the same instant, the
  advisory lock that orders them can time out. The plugin records the gap as a
  `chain_gap` audit event and links nothing, rather than writing a link that
  looks valid and is not. The per-record signature is unaffected. A gap is a
  known, logged state — not evidence of interference.
- **Proxy addresses.** If the site sits behind a content delivery network or load balancer and
  `mmwp_terms_trusted_proxies` was never configured, `ip_address` is the
  proxy's address, and `ip_source` says `remote_addr`. Check that field before
  asserting anything about where a request came from.
- **User-Agent weight.** Corroborating only.
- **Receipt email.** The emailed PDF is supplementary. Delivery is logged
  (`email_sent` / `email_failed`), but delivery to a mail server is not proof
  the person read it. Where an ESP webhook is wired to
  `mmwp_terms_delivery_status_recorded`, provider-side status is available too.

### If verification fails

Do not "fix" anything. There is no repair function, deliberately: re-signing a
record that failed verification would destroy the only evidence that something
happened to it.

1. Note which record IDs failed and how (record signature, context signature,
   or chain link).
2. Preserve a database backup taken before any further change.
3. Check whether the integrity key changed — the most common innocent
   explanation, and one that invalidates every signature at once rather than a
   few.
4. Check the audit events around the affected IDs.
5. Treat a small number of failures against an otherwise valid chain as the
   more serious signal: wholesale failure usually means a key change, while
   isolated failure suggests a specific record was touched.

### Versions published programmatically

The `source` column on each version says how it was published: `settings` (an
admin typed it), `filter` (picked up from an external content source),
`rest`, `cli`, `api` or `code`. Quote it — "the version was published by the
legal team's CMS sync at 03:14 UTC" is a more complete account than "the
version existed".

Two things follow for programmatic publications:

- Where no version number was supplied, one was **derived** (`1.0` → `1.1`, or
  a dated identifier). A derived number is still unique and still names one
  specific document, but it was assigned by the system rather than chosen by a
  person — say so rather than implying a human numbered it.
- Where no effective date was supplied, none is recorded. The field is empty
  rather than inherited from earlier text, which is deliberate: an inherited
  date would be a false claim. An empty effective date is not a gap in the
  evidence — the publication time in `created_at_utc` is authoritative for
  when the version became active.

### Without the admin UI

On installs where the management UI is switched off
(`MMWP_TERMS_DISABLE_ADMIN_UI`), there is no acceptance log screen and no CSV
button. Either re-enable it temporarily on the install, or read the tables
directly:

```bash
wp db query "SELECT a.*, v.version_number, v.effective_date \
  FROM wp_mmwp_terms_acceptances a \
  LEFT JOIN wp_mmwp_terms_versions v ON v.id = a.version_id \
  WHERE a.user_id = 123 ORDER BY a.id DESC" --skip-column-names
```

Reading the table directly gives you no integrity verification. Re-enable the
UI and run the check, or verify the signatures separately, before relying on
what you pull out this way.

The version archive itself is reachable without the UI:

```bash
wp mmwp terms list
wp mmwp terms get --format=json
```


Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/evidence-handling.md

## Privacy

### What is stored

Per acceptance (table `{prefix}mmwp_terms_acceptances`): user ID, login and
e-mail snapshot, the version accepted and its hash, UTC and local time with
timezone, the site identifier, IP address and its source, User-Agent, and two
signatures plus a chain link. Events about a record (receipt delivery,
anonymisation) go to `{prefix}mmwp_terms_audit_events`. Each user also gets
three user-meta markers recording the version they accepted.

No device fingerprinting, MAC address or geolocation is collected.

### Legal basis

GDPR Art. 6(1)(b) and 6(1)(f). Not consent (Art. 6(1)(a)); acceptance is not
withdrawable and records are not deleted on request. See
`dpia-screening.md`, `ropa.md`, `retention-policy.md`.

### Retention

IP address, its source and the User-Agent (the erasable context) are cleared
after the configurable dispute period (default five years) by a daily sweep; the
core record keeps verifying. The anonymisation is audited.

### Personal-data tools

- **Exporter** (Tools → Export Personal Data) lists a user's acceptances.
- **Eraser** anonymises the erasable context where retention allows and
  reports what it retained and why; the core evidence stays.
- **Privacy policy text** is suggested via `wp_add_privacy_policy_content()`.

### Network

The plugin makes no outbound requests, loads no remote assets, and sends no
telemetry. Two kinds of e-mail leave through `wp_mail()`: the receipt to the
accepting user (and an optional address you configure), and a support request
to info@mammothwp.com that an administrator sends from Mammoth WP → Support.
The support request carries a site report only if the administrator ticks
"Include site report" after previewing exactly what it contains.

### Uninstall: what is kept and what is deleted

Always removed when the plugin is deleted: settings, options, scheduled tasks
and transients.

Kept by default (the acceptance evidence): the three tables
(`{prefix}mmwp_terms_acceptances`, `{prefix}mmwp_terms_versions`,
`{prefix}mmwp_terms_audit_events`), the per-user acceptance markers and the
signing key, so kept records can still be verified.

The choice is asked when you click Delete on the Plugins screen ("Keep
acceptance evidence (recommended)" or "Delete all acceptance evidence
permanently", the latter needing the plugin name typed back). It is stored in
the `mmwp_terms_uninstall_data` option and can be changed under Settings →
Terms & Conditions. The filter `mmwp_terms_drop_data_on_uninstall` overrides it.

### Purging evidence now

```bash
wp mmwp terms purge              # report what exists, delete nothing
wp mmwp terms purge --dry-run    # same, explicitly
wp mmwp terms purge --force      # permanently delete the evidence
```

Irreversible: after a purge no acceptance can be reproduced or proved.


Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/privacy.md

## Programmatic API reference

The supported programmatic surface of `mammoth-acceptance-logs`: `Api\TermsApi`
and its REST, WP-CLI and helper-function wrappers, plus every action and
filter the plugin fires.

### Programmatic management

Every route below goes through the same code path, so a version published by
any of them is archived, numbered and audited identically. All of them keep
working with the admin UI switched off — a locked-down install is precisely
the one that has to be driven by code.

**Publishing re-prompts everyone in scope.** That is the point, but it means
publishing on every deploy when nothing changed would nag your users into
ignoring the dialog. Use the "only if changed" variants in automation.

#### Version numbers are derived, never inherited

If you publish without a `version_number`, one is derived — `1.0` becomes
`1.1`, `3` becomes `4`, and a non-numeric or absent number falls back to a
dated identifier, stepping until it finds an unused one. It is never copied
from the version being replaced: two different documents sharing a number is
the exact ambiguity the archive exists to remove, and the settings screen
already refuses to allow it.

The **effective date is never inherited either**. It is a statement about a
specific document, so a version published without one simply has none
recorded, rather than borrowing a date that belonged to different text.

#### PHP

```php
// Publish, asking everyone in scope to accept.
$result = mmwp_terms_publish_version( $html, array(
    'version_number' => '2.1',
    'effective_date' => '2026-10-01',
    'change_summary' => 'Clarified section 4.',
) );

if ( is_wp_error( $result ) ) {
    // Duplicate version number, empty content, bad date, or a failed write.
}

// For sync jobs: no-op when the text already matches the active version.
mmwp_terms_publish_version_if_changed( $html, array( 'version_number' => '2.1' ) );

// Re-read the content source now instead of waiting for the daily cron.
$new_version_id = mmwp_terms_refresh();     // null if nothing changed

mmwp_terms_active_version();                // array|null
mmwp_terms_user_status( $user_id );         // accepted|pending|not_required
```

Call these on or after `plugins_loaded`.

#### WP-CLI, REST and Abilities

These three are declared once with wp-kit (`Api\TermsResource`) and share one
validation and authorisation path. Commands, routes and abilities are listed in
[usage.md](usage.md). Permissions are decided by wp-kit's capability resolver
(`edit_posts` for reads, `manage_options` for publishing, plugin managers for the
acceptance listing) and can be adjusted with its `mmwp_kit_operation_capability` filter.

#### Public read routes

Publishing your terms on a public page needs the text readable without a
login, which the routes above deliberately are not. A second, narrower set
covers that — read-only, versions only, and redacted:

| Route | Method | Purpose |
|---|---|---|
| `/public/version` | GET | The active version, with its full text |
| `/public/versions` | GET | The archive, newest first, *without* the text (`per_page`, `offset`) |
| `/public/versions/<id>` | GET | One version, with its full text |

```
curl https://example.com/wp-json/mmwp/v1/public/version
```

Each response carries `id`, `version_number`, `label`, `effective_date`,
`change_summary` and `is_active`, plus `content` on the single-version routes.
`version_hash`, `source`, `created_at_utc` and the activation window are
**not** exposed: they are internal provenance for whoever audits the archive,
not information a visitor reading the terms needs. Responses are anonymous and
sent with `Cache-Control: public, max-age=300`.

Set the `mmwp_terms_public_api_enabled` filter to `false` to remove these
routes entirely — they then 404 rather than 401, so an install whose terms are
internal does not advertise that the endpoint exists. Doing so also leaves the
Terms & Conditions block with nothing to fetch.

### Actions

| Hook | Fires when | Args |
|---|---|---|
| `mmwp_terms_accepted` | a user accepts | `$user_id, $version_hash, $acceptance_id, $version_id` |
| `mmwp_terms_record_failed` | an acceptance could **not** be persisted | `$user_id, $version_id, $db_error` |
| `mmwp_terms_skipped` | a user clicks "skip for now" | `$user_id` |
| `mmwp_terms_version_published` | a new version is archived and activated | `$version_id, $version_hash` |
| `mmwp_terms_version_changed` | the content hash changes (save or cron) | `$old_hash, $new_hash` |
| `mmwp_terms_content_saved` | the settings page is saved | `$version_hash, $meta` |
| `mmwp_terms_delivery_status_recorded` | integration point for ESP delivery webhooks | `$acceptance_id, $provider_message_id, $status` |

Hook `mmwp_terms_record_failed` and alert someone. Silently losing acceptance
evidence is the worst failure this plugin can have.

### Filters

| Filter | Purpose | Default |
|---|---|---|
| `mmwp_terms_enable_admin_ui` | Remove the whole management UI. See "Headless deployment". | `true` |
| `mmwp_terms_can_manage` | Override who may manage the plugin. Cannot re-enable anything while the kill-switch is on. | in the manager list |
| `mmwp_terms_eligible_managers` | The users offered when appointing a manager. | administrators |
| `mmwp_terms_trusted_proxies` | Proxies whose forwarded-IP header may be trusted (IPs or CIDR ranges). | `[]` |
| `mmwp_terms_ip_header` | Which `$_SERVER` key carries the forwarded client IP. | `HTTP_X_FORWARDED_FOR` |
| `mmwp_terms_site_identifier` | The source application identifier stored on each record. | host + blog ID |
| `mmwp_terms_retention_settings` | Dispute period and whether anonymisation runs. | 5 years, enabled |
| `mmwp_terms_content_source` | Swap the content source entirely — a JSON file, a CPT, an external legal CMS. | the `mmwp_terms_content` option |
| `mmwp_terms_version_meta` | Supply the version number, effective date and change summary for a publication. Runs last and wins — see "Pulling from an external system". | `[]` |
| `mmwp_terms_public_api_enabled` | Whether the unauthenticated `/public/*` read routes are registered at all. The Terms & Conditions block depends on them. | `true` |
| `mmwp_terms_excluded_roles` | Role slugs exempt from the gate. | `[]` |
| `mmwp_terms_excluded_users` | Individual user IDs exempt from the gate. | `[]` |
| `mmwp_terms_send_pdf_email` | Disable the emailed PDF receipt. `($enabled, $user_id)`. | `true` |
| `mmwp_terms_notify_admin_email` | Copy every acceptance notification to this address. | `''` |
| `mmwp_terms_skip_duration` | How long "skip for now" suppresses the overlay. The admin notice is unaffected. | `12 * HOUR_IN_SECONDS` |
| `mmwp_terms_drop_data_on_uninstall` | Whether deleting the plugin also drops the three tables and the acceptance markers. Return false to keep the evidence. | `true` |

#### Example: source the terms from a JSON file instead of the settings page

```php
add_filter( 'mmwp_terms_content_source', function ( string $content ) {
    $path = WP_CONTENT_DIR . '/mmwp-terms/terms.json';
    if ( ! is_readable( $path ) ) {
        return $content;
    }
    $data = json_decode( (string) file_get_contents( $path ), true );
    return (string) ( $data['content'] ?? $content );
} );
```

The daily cron notices the change and archives the new version automatically,
so acceptances against it still point at preserved text.

#### Example: exclude WooCommerce customers, keep staff roles gated

```php
add_filter( 'mmwp_terms_excluded_roles', fn ( array $roles ) => array_merge( $roles, [ 'customer' ] ) );
```

#### Example: record provider-side delivery status from an ESP webhook

```php
// The plugin cannot poll your provider; wire its webhook to this action.
do_action( 'mmwp_terms_delivery_status_recorded', $acceptance_id, $message_id, 'delivered' );
```


Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/programmatic-api.md

## Usage (usage.md)

### Admin

1. **Settings → Terms & Conditions**: write the terms, choose the roles that
   must accept, publish the version.
2. **Users → Terms acceptance log**: the evidence, with signature verification.
   Only plugin managers can open it — being an administrator is not enough.

Users in scope see the popup on their next page load while logged in.

### REST (`mmwp/v1`)

Authenticate like any WordPress request (cookie + nonce, or an application
password). Routes are declared once through wp-kit.

| Method and route | Who |
|---|---|
| `GET /mmwp/v1/terms/version` | `edit_posts` |
| `GET /mmwp/v1/terms/versions?limit=&offset=` | `edit_posts` |
| `GET /mmwp/v1/terms/versions/<id>` | `edit_posts` |
| `POST /mmwp/v1/terms/version` (`content`, `version_number`, `effective_date`, `change_summary`, `only_if_changed`) | `manage_options` |
| `POST /mmwp/v1/terms/refresh` | `manage_options` |
| `GET /mmwp/v1/terms/acceptances?user_id=&version_id=&date_from=&date_to=&limit=&offset=` | plugin managers |

The acceptance listing is read-only and redacted: no IP address, User-Agent or
e-mail. The only unauthenticated routes are read-only version routes under
`/mmwp/v1/public/` (redacted, no acceptance records); disable them with
`mmwp_terms_public_api_enabled`.

### WP-CLI

```bash
wp mmwp terms get
wp mmwp terms list --limit=10 --format=json
wp mmwp terms show 3
wp mmwp terms publish --content="$(cat terms.html)" --version-number=2.1 --effective-date=2026-10-01 --change-summary="Clarified section 4." --only-if-changed
wp mmwp terms refresh
wp mmwp terms acceptances --version-id=3 --format=csv
wp mmwp terms purge --dry-run        # report evidence; add --force to delete it permanently
```

Purge is CLI-only: deleting evidence is deliberately not offered over REST or
abilities.

### Abilities API (WordPress 6.9+)

Category `mmwp-acceptance-logs`: `get-active-version`, `list-versions`,
`get-version`, `publish-version`, `refresh-content` and the read-only
`list-acceptances`. Each carries its field schema and read/write annotations
and enforces the same capability as REST.

### PHP

`mmwp_terms_*()` helpers in `src/api-functions.php` and `Api\TermsApi` are the
supported programmatic entry points. See `programmatic-api.md`.

### Block

Insert **Terms & Conditions** (`mmwp/terms-display`) on a public page to show
the current version, its effective date and history.


Source: https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/usage.md

## Templates

- [DPIA screening — Terms & Conditions acceptance](https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/dpia-screening.md)
- [Privacy notice wording](https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/privacy-notice-snippet.md)
- [Retention policy — acceptance records](https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/retention-policy.md)
- [Record of processing activities — Terms & Conditions acceptance](https://github.com/mammothwp/admin-terms-acceptance/blob/36f6f145beb885d95f3852dfb0faf4d902510f75/docs/ropa.md)
