# Audit trail, members, documents, billing and feedback

Source: https://ethicsportal.eu/developers/administration/

These operations let an organisation’s systems handle routine administration: send the audit trail to a security log, keep the member list in step with HR records, and invite new case handlers. Each one needs its own scope. A personal token also needs its user’s current role, as stated per operation in the [API reference](https://ethicsportal.eu/developers/api/).

| Scope               | Operations                                                           |
| ------------------- | -------------------------------------------------------------------- |
| audit\_log:read     | The organisation’s audit trail and each report’s audit trail         |
| members:read        | Members, and pending invitations                                     |
| members:write       | Invite, resend and revoke invitations                                |
| documents:read      | List and download the channel’s documents                            |
| billing:read        | Read access and subscription status                                  |
| billing:write       | Create subscription checkout and billing portal links                |
| organizations:write | Rename the organisation                                              |
| invitations:respond | List, accept and decline your own invitations (personal tokens only) |

## Read the audit trail

```
curl --fail-with-body --get \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  --data-urlencode 'created_after=2026-11-01T00:00:00Z' \
  "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/audit_logs.json"
```

Each entry says who did what and when: an action name such as `whistleblower.report.viewed`, the actor’s kind, name and membership ID, the subject’s type and ID, and the time. Entries are oldest first. To collect continuously, store the `created_at` of the last entry you received and pass it as `created_after` on the next run; within one run, follow `next_cursor`. An entry appears once it is a minute old, so that a slow write cannot land behind a timestamp you have already passed. As a further safeguard, resume a few minutes before your stored `created_at` and skip entries whose `id` you already have.

The API returns less than the screen, on purpose:

* **No change details, IP addresses or email addresses.** Use the action and subject, then read the subject through its own endpoint.
* **Nothing a reporter did.** A reporter’s visit time can match one employee’s breaks or shifts.
* **No entries about messages, notes or retaliation reports**, consistent with the [data boundary](https://ethicsportal.eu/developers/data-boundaries/).

A personal token reads the audit trail if its user is an administrator or viewer. Every request is itself recorded in the trail.

## Members and invitations

`GET /organizations/{organization_id}/members.json` lists members with their role and status. Use a member’s `id` as `compliance_officer_id` when [configuring the whistleblowing channel](https://ethicsportal.eu/developers/organisation-setup/). Administrators and organisation tokens also see deactivated members.

```
curl --fail-with-body -X POST \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  --data '{"invitation":{"email":"new.handler@example.com","role":"member"}}' \
  "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/invitations.json"
```

The invitee receives an email and accepts in EthicsPortal; the API never returns the acceptance link. Invitations expire after 14 days. Resending restarts that period and is allowed once every two minutes per invitation. Each organisation can send up to 30 invitations per hour.

Changing a member’s role, deactivating or removing a member stays in the web application: granting or withdrawing access to cases is a decision a person makes there. Customers with single sign-on can automate removal through [SCIM provisioning](https://ethicsportal.eu/developers/identity-integrations/).

## Your own invitations

A personal token with `invitations:respond` lists the pending invitations addressed to its user, and accepts or declines them by ID with `PATCH /me/invitations/{id}/accept.json` or `…/decline.json`. Accepting returns the new membership. It suits a consultant who works across several client organisations. An invitation that has expired or was already answered returns `409 invitation_unavailable`.

## Product feedback

Any valid token sends a bug report, feature request or other feedback to the EthicsPortal team. It needs no scope. Feedback from a personal token is attributed to its user, and feedback from an organisation token to that token:

```
curl --fail-with-body \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"feedback":{"kind":"bug","message":"The yearly export times out."}}' \
  "$ETHICSPORTAL_ORIGIN/me/feedback.json"
```

`kind` is `bug`, `feature` or `other` (the default). Only text is accepted; attach screenshots through the in-app form. Feedback reaches EthicsPortal staff, not your organisation, so never include report content or anything that identifies a reporter. Each user can send ten per minute, and so can each organisation across all its tokens.

## Documents

`GET /organizations/{organization_id}/whistleblower/documents.json` lists the documents the token may download, each with its URL and file type. Request each URL with the same `Authorization` header:

```
curl --fail-with-body -o poster.pdf \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/documents/poster.pdf"
```

The policy and privacy notice are editable Word files (`.docx`); the others are PDFs. Documents are generated from the channel’s current settings each time, so publish them again after you change those settings. While the channel is inactive, downloads return `409 channel_inactive`. The compliance report and certificate are audit evidence: a personal token gets them only if its user is an administrator or viewer, and each download is recorded in the audit trail.

## Billing

`GET /organizations/{organization_id}/subscriptions.json` returns whether the organisation has access, whether that access is complimentary, and its subscription status, billing interval and period end. The whistleblowing channel can be active only while `access` is true.

The API never takes a payment itself. With `billing:write` it returns a link to a Stripe-hosted page, and a person completes it there; Stripe handles strong customer authentication, tax and VAT ID collection.

```
curl --fail-with-body -X POST \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  --data '{"subscription":{"interval":"year"}}' \
  "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/subscriptions/checkout.json"
```

* **Subscribe:** `POST …/subscriptions/checkout.json` with `interval` `month` or `year` returns a Checkout `url` and its `expires_at`. Send it to whoever pays. Access starts when Stripe confirms the payment, even if that person never signs in to EthicsPortal. An organisation that already has a subscription gets `409 already_subscribed`.
* **Manage:** `POST …/subscriptions/billing_portal.json` returns a Stripe customer portal link to change the plan, update the payment method, download invoices or cancel. It expires shortly after creation, so create it just before use. Until then it manages the subscription for whoever holds it: treat it like a password. Each link is recorded in the audit trail. Without a Stripe customer yet, it returns `409 no_subscription`.

These suit a partner who sets up organisations for clients, and an assistant that answers “open this link to pay”.

## Rename the organisation

```
curl --fail-with-body -X PATCH \
  -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"organization":{"name":"Example organisation"}}' \
  "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/profile.json"
```
