# EthicsPortal API and webhooks > Developer documentation for the EthicsPortal REST API and signed webhooks: report metadata and deadlines, whistleblowing channel setup, members, documents, billing links and the audit trail. Report content is never available through the API. An AI assistant can set up EthicsPortal for a person through the MCP server. - API base URL: `https://secure.ethicsportal.eu`. JSON endpoints end in `.json`; document downloads return PDF or DOCX. - Authenticate with `Authorization: Bearer `. A personal token (`ep_pat_`) acts with its user's current role in each organisation. An organisation token (`ep_oat_`) belongs to one organisation. Each operation names the scope it requires and the token types that can call it. - Start with `GET /identity.json` to discover the organisations a token can reach. Follow the `Link: rel="next"` header to page through lists, and send an `Idempotency-Key` header when retrying a POST. - OpenAPI 3.1 schema: https://ethicsportal.eu/openapi.json - MCP server: `https://secure.ethicsportal.eu/mcp`. An AI assistant connects with OAuth after a person approves it, and calls the same operations as tools: https://ethicsportal.eu/developers/mcp.md - To set up EthicsPortal for a person from an AI assistant, including a person with no account, follow https://ethicsportal.eu/.well-known/agent-skills/ethicsportal-setup/SKILL.md - Changes are additive: ignore unknown fields, values and webhook event types. Versioning rules and every change: https://ethicsportal.eu/developers/changelog.md - Every page below is Markdown. Each HTML page also answers `Accept: text/markdown`. - All of these pages in one file: https://ethicsportal.eu/developers/llms-full.txt # EthicsPortal developer documentation Source: https://ethicsportal.eu/developers/ The EthicsPortal API lets your systems monitor report status and deadlines, create organisations and configure whistleblowing channels. Webhooks notify your systems when that operational state changes. Report content remains in the authenticated application. API access and webhooks are available to every organisation. Create a token in EthicsPortal to start. 1. [Make your first request](https://ethicsportal.eu/developers/quickstart/). 2. [Choose a token type](https://ethicsportal.eu/developers/authentication/). 3. [Explore the API reference](https://ethicsportal.eu/developers/api/). 4. [Configure webhooks](https://ethicsportal.eu/developers/webhooks/). ## Metadata API and webhooks The metadata API provides four read-only endpoints: organisation discovery, whistleblowing channel metadata, report lists and individual report metadata. Further scopes let a token [create organisations and configure channels](https://ethicsportal.eu/developers/organisation-setup/), [manage webhooks](https://ethicsportal.eu/developers/webhooks/#manage-webhooks-through-the-api), and [read the audit trail, manage members, download documents and read billing status](https://ethicsportal.eu/developers/administration/). [Download the OpenAPI 3.1 schema](https://ethicsportal.eu/openapi.json). Reports expose identifiers, status, assignment presence, workflow timestamps and deadlines. [Review the complete data boundary](https://ethicsportal.eu/developers/data-boundaries/) before designing an integration. Use the API to reconcile current state. Use webhooks for notifications; delivery can be duplicated or arrive out of order. ## Integration surfaces An AI assistant can connect through the [MCP server](https://ethicsportal.eu/developers/mcp/), which offers the API’s operations as tools and has the same data boundary. A person [adds it to their assistant](https://ethicsportal.eu/developers/mcp/#add-ethicsportal-to-an-assistant) and approves the connection; a person with no account can start there. [SAML single sign-on and SCIM provisioning](https://ethicsportal.eu/developers/identity-integrations/) use separate organisation settings and credentials. The metadata API token does not replace a SCIM token. ## Machine-readable files For an AI coding agent, start from [developers/llms.txt](https://ethicsportal.eu/developers/llms.txt). It lists every guide and API operation as Markdown, in reading order. [developers/llms-full.txt](https://ethicsportal.eu/developers/llms-full.txt) has all of them in one file, and the [OpenAPI 3.1 schema](https://ethicsportal.eu/openapi.json) describes every operation, scope and token type. The [public resource directory](https://ethicsportal.eu/developers/public-resources/) lists our other Markdown exports, agent resources, sitemap and compliance questionnaire. Documentation is public and available in English only. ## Markdown content negotiation Append `.md` to a documentation URL, or request it with `Accept: text/markdown`. For example, `/developers/quickstart.md`. These representations contain public documentation only. ## Not-found responses Unknown documentation paths return HTTP 404\. Unsupported representation requests return HTTP 406\. [Content negotiation details](https://ethicsportal.eu/developers/public-resources/#markdown-content-negotiation). ## Other integration boundaries There is no report-content API, programmatic report submission, service-account credential, GraphQL API or SDK in this release. OAuth is used only to connect an AI assistant to the [MCP server](https://ethicsportal.eu/developers/mcp/); the API itself uses tokens. Full report portability uses the authenticated application’s PDF and bulk exports. ## Contact * Integration assistance: [support@ethicsportal.eu](mailto:support@ethicsportal.eu). * Security reports: [security@ethicsportal.eu](mailto:security@ethicsportal.eu) and [security.txt](https://ethicsportal.eu/.well-known/security.txt). --- # Make your first API request Source: https://ethicsportal.eu/developers/quickstart/ ## 1\. Accept your agreements Sign in to EthicsPortal and accept any outstanding account terms or organisation agreement. API requests are refused until you do. ## 2\. Create an API token For an integration your organisation depends on, an administrator opens the organisation, selects **Organisation API tokens**, then **Create API token**. For your own scripts, open your account settings and select **Personal API tokens** instead. [Compare token types](https://ethicsportal.eu/developers/authentication/#choose-a-token-type). Complete the fresh-authentication challenge and give the credential a recognisable name. Keep the preselected read scopes for a monitoring integration. To configure channels or create organisations, follow the [organisation setup guide](https://ethicsportal.eu/developers/organisation-setup/) and select those scopes explicitly. Copy the token immediately: the full value is displayed once. Store it in your integration’s secret store. Tokens expire after 90 days. ## 3\. Discover your organisation Set `ETHICSPORTAL_TOKEN` through your secret manager or a private local environment. The examples use environment-variable references so the token is not written into source code. ``` export ETHICSPORTAL_ORIGIN='https://secure.ethicsportal.eu' curl --fail-with-body \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'Accept: application/json' \ "$ETHICSPORTAL_ORIGIN/identity.json" ``` The response contains the token’s type, name, last four characters, scopes, expiry and last use, and the IDs and names of the organisations it can reach. Use `expires_at` to replace the token before it expires. Copy the required organisation’s `id` into `ETHICSPORTAL_ORGANIZATION_ID`. Treat all IDs as strings; do not decode them or substitute reporter access codes. ## 4\. Fetch report metadata ``` curl --fail-with-body \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'Accept: application/json' \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/reports.json?limit=50" ``` The reports are in the response’s `data` array. An empty `data` array means there are no matching reports you can access. While `next_cursor` is not `null`, request the list again with `cursor` set to that value. [Pagination example](https://ethicsportal.eu/developers/examples/pagination/). ## 5\. Add notifications [Register a webhook](https://ethicsportal.eu/developers/webhooks/) for lifecycle or deadline events. Verify signatures and persist receipt before acknowledging delivery. Periodically use the API to reconcile current state after missed or out-of-order events. ## If the request fails A `401` indicates a missing or unusable token. A `403` can indicate an outstanding agreement. A `404` can mean integrations are disabled or the organisation/report is outside your access. [Errors and rate limits](https://ethicsportal.eu/developers/pagination-and-errors/#errors). --- # Authentication and permissions Source: https://ethicsportal.eu/developers/authentication/ Send the token in the HTTP `Authorization` header on every API request: ``` Authorization: Bearer ``` Use HTTPS and keep credentials out of URLs, logs and source control. There is no report-content or report-write scope. ## Choose a token type | | Personal token | Organisation token | | ----------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- | | Prefix | ep\_pat\_ | ep\_oat\_ | | Belongs to | You | One organisation | | Created in | Account settings → **Personal API tokens** | Organisation → **Organisation API tokens** (administrators only) | | Reaches | Every organisation you belong to, now and in future | Only its organisation | | Report access | Your current role and case permissions in each organisation | Every report’s metadata in its organisation | | When people leave | Stops for an organisation when your membership ends | Keeps working | | Typical use | Your own scripts and account tasks, such as creating organisations | Shared automations, such as a Slack or dashboard integration | Use an organisation token for anything the organisation depends on. It does not break when the person who set it up changes role or leaves, and every administrator can see and revoke it. ## Scopes Each token carries fixed scopes chosen when it is created. Each operation in the [API reference](https://ethicsportal.eu/developers/api/) lists the scope it requires; a request without it returns `403 insufficient_scope`, with a `WWW-Authenticate` header naming the missing scope. | Scope | Allows | Token types | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | | reports:read | List and read report metadata | Both | | channel:read | Read whistleblowing channel status | Both | | channel:write | Read and change channel configuration. [Organisation setup](https://ethicsportal.eu/developers/organisation-setup/) | Both | | webhooks:read | List webhooks and their delivery history | Both | | webhooks:write | Create, change, delete and test webhooks, rotate secrets and retry deliveries. [Webhooks](https://ethicsportal.eu/developers/webhooks/#manage-webhooks-through-the-api) | Both | | audit\_log:read | Read the organisation’s and each report’s audit trail. [Administration](https://ethicsportal.eu/developers/administration/) | Both | | members:read | List members and pending invitations | Both | | members:write | Invite members, resend and revoke invitations | Both | | documents:read | List and download the channel’s documents | Both | | billing:read | Read access and subscription status | Both | | billing:write | Create subscription checkout and billing portal links | Both | | organizations:write | Rename the organisation | Both | | organizations:create | Create organisations | Personal only | | invitations:respond | List, accept and decline your own invitations | Personal only | [Product feedback](https://ethicsportal.eu/developers/administration/#product-feedback) needs no scope: any valid token can send it. Read scopes are preselected; write scopes must be selected explicitly. Scopes cannot be changed after creation: issue a replacement token instead. ## Case permissions A personal token never does more than its owner could do in the web application. Every request rechecks current access. Administrators and viewers can read report metadata throughout their organisation. Members can read only reports assigned to them or in which they participate. Changing channel configuration, managing webhooks or invitations, listing invitations and renaming the organisation require a current administrator; reading the audit trail requires an administrator or viewer. An organisation token has no role. Its scopes alone decide what it can do, and `reports:read` covers every report in the organisation, as an administrator’s view does. Only administrators can create one. ## Which token can call each endpoint Every operation in the [API reference](https://ethicsportal.eu/developers/api/) also lists the token types it accepts under **Authorizations**. A personal token additionally needs its user’s current role, shown in the last column. | Endpoints | Organisation token | Personal token | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- | | [Discover organisations](https://ethicsportal.eu/developers/api/operations/getidentity/) | Yes | Any user | | [Read channel metadata](https://ethicsportal.eu/developers/api/operations/getchannel/) | Yes | Any member | | [List](https://ethicsportal.eu/developers/api/operations/listreports/) and [read](https://ethicsportal.eu/developers/api/operations/getreport/) report metadata | Yes, every report | Administrators and viewers: every report. Members: reports assigned to them or in which they participate | | [Read](https://ethicsportal.eu/developers/api/operations/getchannelconfiguration/) and [change](https://ethicsportal.eu/developers/api/operations/updatechannelconfiguration/) channel configuration | Yes | Administrator | | [Webhooks](https://ethicsportal.eu/developers/webhooks/#manage-webhooks-through-the-api) and their deliveries | Yes | Administrator | | [Organisation audit trail](https://ethicsportal.eu/developers/api/operations/listaudittrail/) | Yes | Administrator or viewer | | [A report’s audit trail](https://ethicsportal.eu/developers/api/operations/listreportaudittrail/) | Yes | Access to that report | | [List members](https://ethicsportal.eu/developers/api/operations/listmembers/) | Yes, including deactivated members | Any member. Only administrators also see deactivated members | | [List](https://ethicsportal.eu/developers/api/operations/listinvitations/), [send](https://ethicsportal.eu/developers/api/operations/createinvitation/), [resend](https://ethicsportal.eu/developers/api/operations/resendinvitation/) and [revoke](https://ethicsportal.eu/developers/api/operations/revokeinvitation/) invitations | Yes | Administrator | | [Rename the organisation](https://ethicsportal.eu/developers/api/operations/updateorganization/) | Yes | Administrator | | [List documents](https://ethicsportal.eu/developers/api/operations/listdocuments/) | Yes | Any member. Compliance evidence is listed only for administrators and viewers | | [Compliance report](https://ethicsportal.eu/developers/api/operations/downloadcompliancereport/) and [certificate](https://ethicsportal.eu/developers/api/operations/downloadcompliancecertificate/) | Yes | Administrator or viewer | | [Policy](https://ethicsportal.eu/developers/api/operations/downloadwhistleblowerpolicy/), [privacy notice](https://ethicsportal.eu/developers/api/operations/downloadprivacynotice/), [poster](https://ethicsportal.eu/developers/api/operations/downloadposter/) and [case handler manual](https://ethicsportal.eu/developers/api/operations/downloadcasehandlermanual/) | Yes | Any member | | [Billing status](https://ethicsportal.eu/developers/api/operations/getbillingstatus/), [checkout](https://ethicsportal.eu/developers/api/operations/createcheckoutlink/) and [billing portal](https://ethicsportal.eu/developers/api/operations/createbillingportallink/) links | Yes | Administrator | | [Create an organisation](https://ethicsportal.eu/developers/api/operations/createorganization/) | No | Any user with an active membership | | [List](https://ethicsportal.eu/developers/api/operations/listmyinvitations/), [accept](https://ethicsportal.eu/developers/api/operations/acceptinvitation/) and [decline](https://ethicsportal.eu/developers/api/operations/declineinvitation/) your own invitations | No | The invited user | | [Send product feedback](https://ethicsportal.eu/developers/api/operations/submitfeedback/) | Yes | Any user | Required organisation agreements apply to both types. Personal tokens also require your current account terms. Organisation discovery can list an organisation whose agreement needs renewed acceptance; reading its report or channel metadata requires acceptance. Tokens do not authenticate the web application. Open a report’s `web_url` in a separate authenticated browser session. Supplying a token alongside a browser cookie does not unlock confidential endpoints. ## Expiry and replacement Tokens expire after 90 days and cannot renew themselves. Generate a replacement, update the consuming system, verify it, then revoke the old token. Full credentials are displayed once when created. Each user, and each organisation, can have up to 20 unexpired, unrevoked API tokens. Revoke an unused token to free a slot. ## Token activity Token lists show **Last used**, or **Never used** if no activity has been recorded. Users see activity for their own tokens; organisation administrators see it for their organisation’s tokens. **Last used** records a request that passed credential, request-shape, account-terms and token-rate checks. It can update even when access to the requested organisation or resource is denied. **Last successful request** records a completed 2xx API response, including authorised setup writes, discovery and HEAD requests. **Last refusal** shows a timestamp and fixed diagnostic code for a request made with that credential. Unknown credentials never update another token’s diagnostics. | Refusal code | Meaning | | ------------------- | --------------------------------------------------------------------------- | | expired / revoked | Replace the expired or revoked token | | unavailable | The owner’s account, or the token’s organisation, can no longer be used | | agreement\_required | Accept the required user terms or organisation DPA in the web application | | invalid\_request | Review JSON body types, allowed fields, query, filters or pagination cursor | | validation\_failed | Correct the fields named in the validation error | | rate\_limited | Respect the response’s retry interval | | insufficient\_scope | The token lacks the scope the operation requires | | forbidden | Current permissions do not allow the action | | not\_found | The resource is missing or inaccessible, or API access is suspended | Each timestamp updates at most once per minute independently. The code describes the refusal sampled at that timestamp; later refusals within that minute may have a different cause. These are operational hints, not a complete request history. Disallowed routes/formats and IP throttling before credential identification do not update them. HTTP errors continue to conceal whether an invalid credential is expired, revoked or unavailable. ## Revocation Users revoke their own personal tokens in account settings. Organisation administrators revoke organisation tokens on the organisation’s token page. An organisation stops a person’s personal tokens from reaching its data by ending that person’s membership. Existing credentials remain available for revocation while API access is suspended. Closing an account removes its personal tokens; organisation tokens and webhooks stay with the organisation. Banned or closed accounts cannot authenticate. ## Separate credentials API tokens, webhook signing secrets, SCIM provisioning tokens and browser sessions serve different purposes. Never interchange them. OAuth access tokens exist only for the [MCP server](https://ethicsportal.eu/developers/mcp/): they work at its endpoint, and the API refuses them. --- # Set up organisations and whistleblowing channels Source: https://ethicsportal.eu/developers/organisation-setup/ Setup uses two write scopes. Neither is preselected: choose them when you create a token. Scopes cannot be added to an existing token. ## Choose the scopes * `organizations:create` creates organisations. Only personal tokens can hold it, because a new organisation belongs to a person: you become its owner and administrator. * `channel:write` reads and changes channel configuration. On a personal token it works only where you are a current administrator; it never promotes a member or viewer. On an organisation token, an administrator granted it when creating the token, so the scope alone applies. Creating a token requires fresh authentication. Tokens cannot issue tokens or accept agreements. Webhooks, invitations and billing links each have their own scope. [Authentication and permissions](https://ethicsportal.eu/developers/authentication/). ## Create an organisation Your token owner must have at least one active membership and must have accepted the current account terms. An [assistant connection](https://ethicsportal.eu/developers/mcp/#start-without-an-organisation) is the exception: a person with no organisation can create their first one through it. ``` curl --fail-with-body \ -X POST \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $(uuidgen)" \ --data '{"organization":{"name":"Example organisation"}}' \ "$ETHICSPORTAL_ORIGIN/organizations.json" ``` A `201` response contains: ``` { "id": "gHiJkl", "name": "Example organisation", "membership_id": "mAbCde", "channel": { "id": "AB12CD34", "active": false } } ``` The token owner becomes the organisation owner and an administrator. Its default whistleblowing channel is created automatically. Treat all IDs as strings. Save `id` as `ETHICSPORTAL_ORGANIZATION_ID` and `membership_id` for appointing yourself as compliance officer. Creation does **not** start a subscription or accept the DPA. The new organisation is reachable through the API at once, but its owner must accept the current organisation agreement in EthicsPortal before configuration requests can succeed. The `Location` response header points to the organisation’s web page, which requires a separate browser session. Send an `Idempotency-Key` header with each creation, as in the example. If the response is lost, repeat the request with the same key: you get the first response back instead of a second organisation. Without a key, a lost response may still mean the organisation was created, so check before retrying. [Safe retries](https://ethicsportal.eu/developers/pagination-and-errors/#safe-retries). Names need not be unique. Organisation creation allows up to ten attempts per user per minute, shared across tokens. ## Read the configuration ``` curl --fail-with-body \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/portal/configuration.json" ``` This privileged resource returns company details, channel settings, the compliance officer’s obfuscated membership ID, public reporting form URL, verification state, `missing_fields` and derived `active` state. It requires `channel:write` even for GET. Ordinary channel metadata remains limited to its identifier, active state and public form URL. ## Configure the channel Send only the fields you want to change. Use native JSON booleans and numbers. Unknown attributes, nested values, additional top-level objects and query parameters are rejected with `400 invalid_request`. ``` curl --fail-with-body \ -X PATCH \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"portal":{ "legal_name":"Example company", "country_code":"FR", "registration_number":"EXAMPLE-123", "address":"1 Example Street", "employee_range":"1-49", "industry":"professional_scientific_services", "retention_months":60, "compliance_officer_id":"mAbCde", "social_dialogue_consulted":true, "voice_recording_enabled":false, "email_intake_enabled":false, "brand_color":"#123456" }}' \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/portal/configuration.json" ``` Additional optional fields are `phone_number` and `website`. [The API reference](https://ethicsportal.eu/developers/api/) lists types and supported values. Use the `membership_id` returned by creation to appoint yourself. Other officers must already be active administrators in the same organisation; obtain their obfuscated membership ID from your authorised team workflow. Raw database IDs are rejected. Settings obey the same model rules as the application: * Verified company details are locked. The API cannot grant or remove verification. * Retention must be an offered value and meet the country’s minimum. Change the country and retention together when needed. * Compliance officers must be active administrators in the organisation. * Removing required information, including setting the compliance officer to `null`, can make the channel inactive. * `active` is derived from complete required information and existing subscription or granted access. It cannot be set directly. A successful save does not guarantee activation. The API does not create missing channels during GET or PATCH. A normal organisation creation already creates one; a missing channel returns `404`. ## Handle validation errors A model validation failure returns `422` and the affected field names, without echoing submitted values: ``` {"error":{"code":"validation_failed","fields":["retention_months"],"feedback_url":"https://secure.ethicsportal.eu/me/feedback.json"}} ``` `base` indicates a record-level restriction, such as locked verified company details. A non-administrator compliance officer can be reported as `compliance_officer`. No partial settings are saved on validation failure. [Errors and rate limits](https://ethicsportal.eu/developers/pagination-and-errors/#errors). All report-content exclusions continue to apply. There is no report submission, report mutation or agreement-acceptance API. After setup, the API also manages [webhooks](https://ethicsportal.eu/developers/webhooks/#manage-webhooks-through-the-api), and sends invitations and creates subscription checkout links: [Administration](https://ethicsportal.eu/developers/administration/). Changing a member’s role and removing a member stay in the web application. --- # Data boundaries Source: https://ethicsportal.eu/developers/data-boundaries/ The API supports operational monitoring and explicitly authorised organisation and whistleblowing channel setup. It cannot read report content or submit or change reports. The [MCP server](https://ethicsportal.eu/developers/mcp/) offers the same operations as tools, so the same boundary applies to it. ## Included report fields The [generated reference](https://ethicsportal.eu/developers/api/) is the field-by-field contract. Report responses contain: * Obfuscated report and organisation IDs. * Workflow status: `received`, `acknowledged`, `feedback_given` or `closed`. * Whether an assignee exists, without their identity. * Creation, acknowledgement, feedback and closure timestamps. * Acknowledgement and feedback deadlines and overdue flags. * API and authenticated web links. Dates use UTC ISO-8601\. These timestamps describe recorded workflow state. Closing a report can fill missing acknowledgement and feedback timestamps; reopening can clear automatically filled values. They do not prove a message was delivered or a legal obligation was fulfilled. ## Always excluded Excluded fields are omitted entirely. They are never returned as `REDACTED`, and there is no expansion option. * Subject, description, summaries, translations and intake answers. * Reporter identity, contact information and access credentials. * Attachments, recordings and their metadata. * Messages and notes, including their counts. * Category, closure outcome and retaliation indicators. * Priority, source, language, confirmation and assignee identities. * Audit metadata, notification text, PDFs and exports. There is no `updated_at` field or filter that reveals hidden content activity. Changes confined to excluded content do not emit webhook events. ## Handle metadata appropriately Operational metadata can still identify a case inside your organisation. Restrict access to the receiving system and store only what the integration needs. Report links require a separate authenticated browser session. ## Audit trail The [audit trail](https://ethicsportal.eu/developers/administration/#read-the-audit-trail) returns who did what and when, without change details, IP addresses or email addresses. It omits anything a reporter did and every entry about messages, notes or retaliation reports, so their existence, number and timing stay inside the application. ## Whistleblowing channel metadata The channel endpoint returns its identifier, active state and public online reporting form URL. It does not expose handler identities or private settings. A separate [configuration resource](https://ethicsportal.eu/developers/organisation-setup/) requires the `channel:write` scope; it includes company details, settings and the compliance officer’s obfuscated membership ID. Those fields never appear in report metadata or webhook snapshots. --- # Pagination, filters and errors Source: https://ethicsportal.eu/developers/pagination-and-errors/ ## Pagination Lists are objects, never bare arrays: records are in `data`, and `next_cursor` is the cursor for the next page, or `null` on the last page. ``` {"data":[{"id":"aBcDef","status":"received"}],"next_cursor":"SIGNED_CURSOR"} ``` Organisation discovery, report lists and webhook delivery lists accept `limit` (default 50, maximum 100) and `cursor`. Request the same URL again with `cursor` set to the `next_cursor` value until it is `null`. The same next page is also in the `Link` response header, for clients that follow RFC 8288 links. Discovery returns its organisations in `organizations`, with `next_cursor` beside it. Cursors expire after one hour and are bound to the credential, resource, organisation and filters. Restart the traversal if a cursor expires; deduplicate report IDs when reconciling. New records created after the first page appear in a fresh traversal. Permissions and workflow state can change between requests. Pagination is not a database snapshot. [Use the Python pagination example](https://ethicsportal.eu/developers/examples/pagination/), which only ever requests the report collection on the EthicsPortal origin. ## Links Responses link to where you can go next, in fields ending in `_url`. Each organisation in `/identity.json` links to its reports, channel, members, webhooks, audit trail, documents and billing. A report links to its audit trail, the channel to its configuration and documents, a webhook to its deliveries, and your invitations to their accept and decline actions. Follow these links rather than building URLs. The one exception to the naming: a webhook’s own `url` is its destination, as in Stripe’s API. ## Conditional requests Every successful JSON read carries an `ETag`. Send it back as `If-None-Match`, and an unchanged resource returns `304 Not Modified` with no body: ``` curl -sS -o /dev/null -w '%{http_code}\n' \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'If-None-Match: W/"0ba6c64b35971e64b16534c1f240e4a0"' \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/reports.json" ``` Use it when polling a dashboard or re-reading a resource in an assistant’s context: an unchanged response costs no transfer and no context. It still counts toward rate limits and, for audited reads, the audit trail. ## Report filters | Parameter | Meaning | | --------------- | ------------------------------------------------------ | | status | One of received, acknowledged, feedback\_given, closed | | created\_after | Exclusive ISO-8601 creation-time lower bound | | created\_before | Exclusive ISO-8601 creation-time upper bound | ``` curl --fail-with-body --get \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ --data-urlencode 'status=received' \ --data-urlencode 'created_after=2026-11-01T00:00:00Z' \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/whistleblower/reports.json" ``` Unknown parameters, array values, arbitrary sorting, content filters and expansions are rejected. Single-resource endpoints do not accept list filters. ## Errors ``` {"error":{"code":"invalid_token","feedback_url":"https://secure.ethicsportal.eu/me/feedback.json"}} ``` Every error has a `code` and a `feedback_url`. A validation error also has `fields`. `feedback_url` is the address of the [product feedback](https://ethicsportal.eu/developers/administration/#product-feedback) operation. If an error looks wrong, or the documentation did not help, send a short description there with the same token. | Status | Code | Action | | ------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | 400 | invalid\_request | Correct the parameters or restart an expired cursor traversal | | 401 | invalid\_token | Check the credential, expiry, revocation and owner access | | 403 | insufficient\_scope | Issue a token with the scope named in the WWW-Authenticate header | | 403 | forbidden or agreement\_required | The user’s role does not allow the action, or an agreement must be accepted in the web application | | 404 | not\_found | Check the resource ID and current membership or case access | | 409 | idempotency\_key\_reused, idempotency\_key\_in\_progress or not\_retryable | Use a new key for a different request, wait for the first request, or retry only failed deliveries | | 409 | channel\_inactive or invitation\_unavailable | Activate the whistleblowing channel first, or ask for a new invitation | | 409 | already\_subscribed or no\_subscription | Use a billing portal link instead, or create a checkout link first | | 422 | validation\_failed | Correct the named fields; submitted values are not echoed | | 429 | rate\_limited | Wait for the Retry-After interval before retrying | ## Rate limits Limits are 120 eligible requests per minute per source IP, 60 per token and 300 per organisation. Organisation creation has an additional ten-attempt limit per user per minute across tokens. A `429` response includes `Retry-After: 60`. Avoid concurrent retry bursts. Bound retries for network or server failures and use backoff; do not continuously retry invalid credentials or forbidden access. Responses use `Cache-Control: no-store`. Reads support GET and HEAD. Only the documented resources accept POST, PATCH or DELETE, with the matching write scope. PUT and all report writes remain unavailable. A missing scope returns `403` with a header naming it: ``` WWW-Authenticate: Bearer realm="EthicsPortal", error="insufficient_scope", scope="webhooks:write" ``` ## Safe retries Send an `Idempotency-Key` header with every POST, such as a UUID you generate per operation. If the response is lost, repeat the same request with the same key: within 24 hours you get the first response again, marked `Idempotent-Replayed: true`, instead of a second organisation, webhook or test event. * Successful responses and `422` validation errors are stored and replayed. Any other outcome releases the key, so the retry runs normally. * Reusing a key with a different method, path or body returns `409 idempotency_key_reused`. * A retry while the first request is still running returns `409 idempotency_key_in_progress`; wait and retry with the same key. * Keys belong to the token that sent them and may contain up to 255 printable ASCII characters. PATCH and DELETE requests are already safe to repeat. --- # Webhooks Source: https://ethicsportal.eu/developers/webhooks/ ## Configure a destination An administrator opens **Organisation → Integrations → Webhooks**, registers a public HTTPS destination on port 443 and selects events. Each organisation can have up to ten destinations. Webhooks belong to the organisation, not to the administrator who created them. They receive events for every report in the organisation and keep delivering when staff change. Only administrators can see, create or operate them. Copy the signing secret when it is displayed after creation. Store it in the receiver’s secret store. Use **Send test event** to queue a `webhook.test` event containing only the organisation ID. A queued event is not proof of delivery; inspect delivery history. ## Manage webhooks through the API Everything on the webhook screen is also available to a token with `webhooks:read` and `webhooks:write`: list, create, read, update and delete webhooks, rotate the signing secret, send a test event, list deliveries and retry a failed delivery. A personal token also needs its user to be a current administrator. The [API reference](https://ethicsportal.eu/developers/api/) lists each operation. ``` curl --fail-with-body -X POST \ -H "Authorization: Bearer $ETHICSPORTAL_TOKEN" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $(uuidgen)" \ --data '{"webhook":{"name":"Case dashboard","url":"https://receiver.example/ethicsportal","events":["report.created","report.overdue"]}}' \ "$ETHICSPORTAL_ORIGIN/organizations/$ETHICSPORTAL_ORGANIZATION_ID/webhooks.json" ``` The `201` response contains the signing secret once, as does rotating it; reading a webhook never returns it. Store it before doing anything else. If the response is lost, repeat the request with the same `Idempotency-Key` to receive it again within 24 hours. ## Events | Event | Meaning | | -------------------------- | -------------------------------------------------- | | report.created | A report was created | | report.acknowledged | An acknowledgement timestamp was recorded | | report.feedback\_given | A feedback timestamp was recorded | | report.closed | A report was closed, without the closure outcome | | report.reopened | A report was reopened | | report.assignment\_changed | Assignment changed between present and absent | | report.due\_soon | An outstanding deadline entered the two-day window | | report.overdue | An outstanding deadline passed | Replacing one assignee with another does not emit an event because assignment remains present. Closure emits one closure event instead of extra events for automatically filled acknowledgement/feedback timestamps. Messages, notes, files and other excluded content do not generate events. ## Payloads Every envelope contains `id`, `type`, `created_at` and `data`. Lifecycle events contain the report metadata snapshot at event creation, without `url` and `web_url`. Deadline data contains only `report_id`, `organization_id`, `deadline_type` and `deadline_at`. [Read the generated webhook schema](https://ethicsportal.eu/developers/api/webhooks/receivereportevent/) for exact fields and event variants. Snapshots can differ from current state by delivery time. Subscriptions do not receive events from before their creation. Deadline events deduplicate by report, event type, deadline kind and due time within the 30-day retention window. An unresolved deadline may produce another reminder after the earlier event has expired. ## Verify deliveries ``` X-EthicsPortal-Event-Id: X-EthicsPortal-Signature: t=,v1= ``` Compute HMAC-SHA256 using your secret over the timestamp, a full stop and the **exact raw request bytes**. Compare the hexadecimal digest in constant time. Reject timestamps more than five minutes from your clock, then parse the JSON and require its `id` to match the event header. [Use the tested Node.js verification example](https://ethicsportal.eu/developers/examples/verify-webhook/). Do not parse and re-serialize JSON before signature verification. ## Acknowledge and deduplicate Persist a verified event in a durable inbox with a unique event ID before returning any `2xx` response. If that event already exists, acknowledge it without repeating its effects. Perform business processing asynchronously from that inbox. If persistence fails, return a non-2xx status so delivery can retry. Delivery is at least once. There is no ordering guarantee. A lost response can cause a duplicate with the same event ID/body and a new signature timestamp. Reconcile current state through the API after missed or out-of-order events. ## Retries and history Non-2xx responses and connection failures retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: seven attempts over about 32 hours 36 minutes, plus queue delays. Unsafe destinations fail permanently. Redirects are not followed. The endpoint screen shows the latest 100 deliveries with the stable event ID, status, attempts, HTTP status or a fixed error code. For a pending retry, it shows the next scheduled attempt time. This is the earliest eligible time; queue delays can postpone it. Paused retries are labelled when integrations are disabled. Match the event ID to your receiver logs; manual retries retain it. Failed deliveries can be retried manually while retained. A manual retry uses the endpoint’s current URL and signing secret, so retry after fixing your receiver. Events and delivery history expire after 30 days. Receiver response bodies are not retained. Deleting a report removes its retained events; deleting an endpoint removes its delivery history. ## Failure warnings When a delivery fails permanently or exhausts its retries, the webhook displays a warning. Organisation administrators receive an in-app notification and email. Notifications contain a generic warning and a link to delivery history, without report data, the destination URL or the signing secret. Email checks current access and failure state again before sending. Further failed deliveries do not create another alert during the same failure episode. The warning clears after a successful delivery or a change to the destination, secret, event selection or enabled state. A manual retry alone does not clear it. A later failure after recovery can create a new alert. Email delivery is asynchronous and may repeat if a job is recovered after an interrupted send. **Failure does not automatically disable the webhook.** New events continue to be attempted. Inspect the receiver, correct the configuration if necessary, and send a test event. Reconcile missed state through the API. ## Rotation and access changes Generate a replacement secret in the endpoint screen and update the receiver. There is no overlap period. Changing the URL, secret, event selection or enabled state invalidates pending deliveries from the previous configuration. Send a fresh test event and reconcile through the API after changes. If API access is suspended, sends pause and event capture stops. An in-flight HTTP request cannot be recalled. When access is restored, retained pending deliveries resume; report changes made during the suspension are not backfilled. The organisation’s agreement is checked again before each delivery. Existing endpoints can still be deleted during a suspension. --- # 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" ``` --- # MCP server for AI assistants Source: https://ethicsportal.eu/developers/mcp/ The EthicsPortal MCP server lets an AI assistant work with EthicsPortal for a person who approves it. It offers the operations of the [API](https://ethicsportal.eu/developers/api/) as tools of the Model Context Protocol (MCP). The assistant acts as that person, with their current role, only in the organisations they choose. Report content is never available. ## Status The MCP server is available to every organisation that has API access. It opened on 11 November 2026. * EthicsPortal has tested it with Claude Code. Other assistants that support remote MCP servers with OAuth are expected to work. Write to [support@ethicsportal.eu](mailto:support@ethicsportal.eu) if one does not. * If API access is suspended for an organisation, the person cannot choose it on the consent screen, and its tools answer `not_found`. * If EthicsPortal switches assistant connections off, the MCP endpoint and every OAuth address on this page answer `404`. The REST API and webhooks do not depend on the MCP server. [Choose an API token](https://ethicsportal.eu/developers/authentication/) for scripts and server integrations. ## Add EthicsPortal to an assistant A person adds the server to their assistant once. They need no EthicsPortal account first: a person with no account [signs up while they connect](https://ethicsportal.eu/developers/mcp/#start-without-an-organisation). | Assistant | How to add the server | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Claude Code | Run claude mcp add --transport http ethicsportal https://secure.ethicsportal.eu/mcp. Then open /mcp, choose ethicsportal and **Authenticate** | | Cursor | Add the server to \~/.cursor/mcp.json, as shown below. Sign in when Cursor asks | | Claude | **Settings → Connectors → Add custom connector**. Enter the address https://secure.ethicsportal.eu/mcp | | Any other assistant | Add https://secure.ethicsportal.eu/mcp as a remote MCP server or custom connector in the assistant’s settings | ``` { "mcpServers": { "ethicsportal": { "url": "https://secure.ethicsportal.eu/mcp" } } } ``` The assistant then opens a browser window. The person signs in or signs up there, reads the consent screen and presses **Connect**. An assistant cannot use a server that nobody added. A coding assistant can add the server itself, because it can run the command or change its configuration. A chat assistant cannot: the person adds the connector in its settings. The [setup skill](https://ethicsportal.eu/.well-known/agent-skills/ethicsportal-setup/SKILL.md) gives an assistant these steps, and the steps that follow, in a form it can read. ## Endpoint and transport | Property | Value | | ------------- | ------------------------------------------------------------------------- | | Endpoint | https://secure.ethicsportal.eu/mcp | | Transport | Streamable HTTP, POST only | | Request | One JSON-RPC message per request. A batch is refused | | Response | One JSON document, or no body. The server opens no stream | | Session | None. The server issues no Mcp-Session-Id | | Authorization | Authorization: Bearer with an OAuth access token issued for this endpoint | | Features | Tools only. No prompts and no resources | Send `Content-Type: application/json` and `Accept: application/json, text/event-stream` with every request. Two lifecycles work: * **Revision `2026-07-28`**, without a handshake. Each request carries its protocol revision, and `server/discover` describes the server. * **Revisions `2025-11-25` and `2025-06-18`**, with the `initialize` handshake. Send the `MCP-Protocol-Version` header on the requests that follow. A client that asks for `2026-07-28` in `initialize` is offered `2025-11-25`. The server returns short instructions for the assistant in both lifecycles. They tell it to start with `get_identity`. ``` curl --fail-with-body https://secure.ethicsportal.eu/mcp \ -H "Authorization: Bearer $ETHICSPORTAL_MCP_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2026-07-28' \ -H 'Mcp-Method: tools/call' \ -H 'Mcp-Name: get_identity' \ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_identity","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' ``` Most clients do not build these requests themselves. Add the endpoint address as a remote MCP server in the assistant. The assistant must support OAuth 2.1 with PKCE and Dynamic Client Registration. ## Connect a client A connection is made with OAuth 2.1\. The client is a public client: it has no secret. The steps are discovery, registration, the authorization request, and the token exchange. ### Discovery A request without a usable token answers `401`. The `WWW-Authenticate` header names the protected resource metadata (RFC 9728): ``` WWW-Authenticate: Bearer scope="reports:read channel:read channel:write webhooks:read webhooks:write audit_log:read members:read members:write documents:read billing:read billing:write organizations:write organizations:create", resource_metadata="https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp" ``` ``` curl --fail-with-body https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp ``` ``` { "resource": "https://secure.ethicsportal.eu/mcp", "authorization_servers": ["https://secure.ethicsportal.eu"], "scopes_supported": [ "reports:read", "channel:read", "channel:write", "webhooks:read", "webhooks:write", "audit_log:read", "members:read", "members:write", "documents:read", "billing:read", "billing:write", "organizations:write", "organizations:create" ], "bearer_methods_supported": ["header"] } ``` `scopes_supported` lists every scope a connection can hold, here and in the authorization server metadata. The same document is also at `/.well-known/oauth-protected-resource`. The authorization server metadata (RFC 8414) is at the issuer: ``` curl --fail-with-body https://secure.ethicsportal.eu/.well-known/oauth-authorization-server ``` The response includes these fields: ``` { "issuer": "https://secure.ethicsportal.eu", "authorization_endpoint": "https://secure.ethicsportal.eu/oauth/authorize", "token_endpoint": "https://secure.ethicsportal.eu/oauth/token", "revocation_endpoint": "https://secure.ethicsportal.eu/oauth/revoke", "registration_endpoint": "https://secure.ethicsportal.eu/oauth/register", "scopes_supported": [ "reports:read", "channel:read", "channel:write", "webhooks:read", "webhooks:write", "audit_log:read", "members:read", "members:write", "documents:read", "billing:read", "billing:write", "organizations:write", "organizations:create" ], "response_types_supported": ["code"], "response_modes_supported": ["query"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["none"], "code_challenge_methods_supported": ["S256"], "authorization_response_iss_parameter_supported": true, "resource_indicators_supported": true } ``` There is no token introspection endpoint. ### Register the client Clients register themselves with Dynamic Client Registration (RFC 7591). Client ID Metadata Documents are not supported yet. ``` curl --fail-with-body https://secure.ethicsportal.eu/oauth/register \ -H 'Content-Type: application/json' \ --data '{"client_name":"Example assistant","redirect_uris":["https://assistant.example/callback"],"token_endpoint_auth_method":"none","grant_types":["authorization_code","refresh_token"],"response_types":["code"]}' ``` A `201` response contains the `client_id`. There is no client secret. ``` { "client_id": "CLIENT_ID", "client_id_issued_at": 1794268800, "client_name": "Example assistant", "redirect_uris": ["https://assistant.example/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` | Field | Accepted | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | redirect\_uris | Required. One to ten addresses, 4,000 characters in total | | client\_name | Optional. Up to 100 characters, without control or invisible characters. Without a name, the host of the first redirect address is used | | token\_endpoint\_auth\_method | Omitted, or none | | grant\_types | Omitted, or only authorization\_code and refresh\_token | | response\_types | Omitted, or only code | Every other field is ignored and is not stored. A redirect address must use `https`, or `http` on a loopback host: `localhost`, `127.0.0.1` or `::1`. It must not contain user information or a fragment, and it must not point to an EthicsPortal host. Custom URL schemes are refused. The only exception is the exact address `cursor://anysphere.cursor-mcp/oauth/callback`; no other address with that scheme is accepted. A refused address answers `400` with `invalid_redirect_uri`. Any other refused field answers `400` with `invalid_client_metadata`. A registration that has no connection is deleted when it is 30 days old. If a client ID is no longer recognised, register again. ### Ask for authorization Send the person’s browser to the authorization endpoint: ``` https://secure.ethicsportal.eu/oauth/authorize ?response_type=code &client_id=CLIENT_ID &redirect_uri=https%3A%2F%2Fassistant.example%2Fcallback &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 &resource=https%3A%2F%2Fsecure.ethicsportal.eu%2Fmcp &scope=reports%3Aread%20channel%3Aread &state=STATE ``` | Parameter | Rule | | ---------------------------------------- | --------------------------------------------------------------------------------------------------- | | response\_type | code only | | redirect\_uri | A registered address. For a loopback address, the port can differ from the registered one | | code\_challenge, code\_challenge\_method | PKCE is required, with S256 only. plain is refused | | resource | Required. Exactly https://secure.ethicsportal.eu/mcp | | scope | Optional. Without it, the request asks for every scope a connection can hold. See [Scopes](https://ethicsportal.eu/developers/mcp/#scopes) | | response\_mode | Omitted, or query | A request that breaks one of these rules is refused on an error page in EthicsPortal. The browser is not sent back to the client, so the client receives no error redirect. ### What the person sees 1. The person signs in to EthicsPortal if they are not signed in, and completes a fresh-authentication challenge. A person with no account creates one at this step: they confirm their email address, accept the account terms and enter their name. Then they return to the same request. 2. The consent screen shows the name of the app and the host of its redirect address. It states that the app chose both and that EthicsPortal has not verified them. 3. It lists the permissions the app asks for, in plain language, and the limits of a connection. 4. It lists the organisations the person can connect. These are the organisations where the person is an active member and API access is on. The person ticks at least one. 5. **Connect** sends the browser back to the redirect address with `code`, `state` and `iss`. **Cancel** sends it back with `error=access_denied`. Consent is asked every time. An earlier connection never approves a new request by itself. After each approval, the person receives one email. It names the app and its address, the time, the organisations, the permissions and the end date, and it links to **Connected apps**, where the person can end the connection. ### Start without an organisation A person who has no organisation in EthicsPortal can connect an assistant, and the assistant creates their first organisation. 1. The authorization request must include `organizations:create`. A request that follows the metadata includes it. 2. The consent screen shows no list of organisations. It says that the app can create one, and that the person accepts its Data Processing Agreement afterwards. 3. The connection reaches no organisation at first. `get_identity` returns an empty `organizations` list. 4. `create_organization` creates the organisation with the person as its owner, and adds it to the connection. 5. The owner accepts the organisation’s Data Processing Agreement in a browser, at `https://secure.ethicsportal.eu/organizations/{organization_id}`. Until then, the organisation’s tools answer `agreement_required`. The assistant cannot accept it. 6. After that, the assistant can configure the whistleblowing channel, invite members and create a checkout link, with the scopes the person approved. The tool results guide the assistant through steps 3 to 5\. The empty `get_identity` result, the `create_organization` result and every `agreement_required` answer carry a `next_step` text: what to do next, and the address the person must open. The text is in the result’s content and in `structuredContent.next_step`. The REST API does not return this field. ### Exchange the code The authorization code is valid for one minute and for one use. ``` curl --fail-with-body https://secure.ethicsportal.eu/oauth/token \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode "code=$CODE" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode 'redirect_uri=https://assistant.example/callback' \ --data-urlencode "code_verifier=$CODE_VERIFIER" \ --data-urlencode 'resource=https://secure.ethicsportal.eu/mcp' ``` ``` { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "REFRESH_TOKEN", "scope": "reports:read channel:read", "created_at": 1794268800 } ``` The access token works at the MCP endpoint only. The REST API refuses it with `401`. A `resource` other than the MCP endpoint answers `invalid_target`. ### Refresh and revoke tokens ``` curl --fail-with-body https://secure.ethicsportal.eu/oauth/token \ --data-urlencode 'grant_type=refresh_token' \ --data-urlencode "refresh_token=$REFRESH_TOKEN" \ --data-urlencode "client_id=$CLIENT_ID" ``` Each refresh returns a new access token and a new refresh token. The earlier access token and refresh token stop working at once. Store the new pair before you use it. * A refresh cannot change the scopes. If you send `scope`, it must name exactly the scopes of the connection, or the answer is `invalid_scope`. * A refresh token works once. There is no period in which the earlier one is still accepted. * Other grant types answer `unsupported_grant_type`. To end a connection from the client, revoke its current access token or refresh token (RFC 7009): ``` curl --fail-with-body https://secure.ethicsportal.eu/oauth/revoke \ --data-urlencode "token=$REFRESH_TOKEN" \ --data-urlencode "client_id=$CLIENT_ID" ``` A client can revoke only its own tokens. Another client’s token answers `403`. ### Lifetimes | Item | Lifetime | | ------------------ | ------------------------------------------------------- | | Authorization code | One minute, one use | | Access token | One hour | | Refresh token | One use. Each refresh returns a new one | | Connection | 90 days from the approval. A refresh does not extend it | ### Browser-based clients A client that runs in a web page on another origin can connect. The MCP endpoint, both metadata documents and the registration, token and revocation endpoints answer with `Access-Control-Allow-Origin: *` and answer an `OPTIONS` preflight with `204`. They never use cookies, and they send no `Access-Control-Allow-Credentials` header. The MCP endpoint and the protected resource metadata expose the `WWW-Authenticate` and `Retry-After` headers to the page. The consent screen has no such headers. Open it as a page in the person’s browser. ## What ends a connection When a connection ends, its access tokens answer `401` and its refresh tokens answer `invalid_grant`. The person must approve a new authorization request. * 90 days have passed since the approval. * The person disconnects the app under **Connected apps**. * An administrator removes the connection from the last organisation it reaches. * The person’s membership is deactivated in the last organisation it reaches. * The client revokes its current access token or refresh token. * **A refresh token is used a second time.** This includes a client that repeats a refresh request after a lost response. * An authorization code is exchanged a second time with its PKCE verifier. * The person approves a new request from the same app. The new connection replaces the earlier one, and the 90 days start again. A connection stops working, without ending, while the person’s account cannot authenticate or while assistant connections are switched off. ## What a connection can do A connection acts as the person who approved it. Every tool call checks that person’s current role and case permissions again, as described in [Case permissions](https://ethicsportal.eu/developers/authentication/#case-permissions). A connection never does more than the person can do in the web application. * It reaches only the organisations the person ticked. Any other organisation answers `not_found`, also when the person is a member of it. * An organisation that the person joins later is not added. The one exception is an organisation created with the `create_organization` tool of that connection. * When the person’s membership in an organisation is deactivated, that organisation is removed from the connection. * The person’s account terms and the organisation’s agreement must be accepted, as for the API. Otherwise the tool answers `agreement_required`. Tool descriptions come from the API reference. Where a description says “personal token”, it applies to a connection in the same way. ### Scopes A connection uses the scopes of the API. The [scope table](https://ethicsportal.eu/developers/authentication/#scopes) describes each one. A connection can hold these: `reports:read`, `channel:read`, `channel:write`, `webhooks:read`, `webhooks:write`, `audit_log:read`, `members:read`, `members:write`, `documents:read`, `billing:read`, `billing:write`, `organizations:write`, `organizations:create` * `invitations:respond` cannot be granted. Accepting an invitation joins an organisation that the person did not choose on the consent screen. A request that asks for it is refused. * `get_identity` and `submit_feedback` need no scope. * `members:write` lets the app send invitations with any role, in the organisations where the person is an administrator. A person who is invited as an administrator can read reports in the web application after they accept. The consent screen warns the person when an app asks for this scope, and each invitation is recorded in the audit trail as made through an AI assistant. * The metadata advertises every scope, so a client that follows it asks for all of them in the first authorization request. Most assistants make the person sign in again for each scope added later. A client can ask for fewer scopes. The consent screen always shows every scope the client asks for. ### Ask for a further scope A connection that asked for fewer scopes can ask for more later. When a tool needs a scope the connection does not have, nothing runs. The whole HTTP response is `403`: ``` WWW-Authenticate: Bearer error="insufficient_scope", error_description="This tool needs the webhooks:write scope.", scope="reports:read channel:read webhooks:write", resource_metadata="https://secure.ethicsportal.eu/.well-known/oauth-protected-resource/mcp" ``` 1. Read `scope`. It names the scopes the connection already has and the missing one. 2. Send a new authorization request with exactly that `scope`. 3. The person approves on the consent screen. The organisations of the earlier connection are ticked already. 4. Exchange the new code, then call the tool again with the new access token. The new connection replaces the earlier one. The earlier tokens stop working, the 90 days start again, and the person receives a new email. ## Tools The server offers one tool for each API operation, with the exceptions listed below. Tool definitions are generated from the [OpenAPI schema](https://ethicsportal.eu/openapi.json), so a tool has the same fields, rules and errors as its operation. Follow the link of a tool to its page in the API reference. * The tool name is the `operationId` of the operation in snake case: `listReports` becomes `list_reports`. * The tool title and description are the summary and description of the operation. * Tools that read are marked read-only. Tools that send `PATCH` or `DELETE` are marked destructive and idempotent. * `tools/list` returns the tools in the order of the OpenAPI schema. | Tool | API operation | Scope | | ------------------------------ | -------------------------------------------------------------------------------------------- | -------------------- | | get\_identity | [Discover organisations](https://ethicsportal.eu/developers/api/operations/getidentity/) | None | | get\_channel | [Read whistleblowing channel metadata](https://ethicsportal.eu/developers/api/operations/getchannel/) | channel:read | | list\_reports | [List report metadata](https://ethicsportal.eu/developers/api/operations/listreports/) | reports:read | | get\_report | [Read report metadata](https://ethicsportal.eu/developers/api/operations/getreport/) | reports:read | | create\_organization | [Create an organisation](https://ethicsportal.eu/developers/api/operations/createorganization/) | organizations:create | | get\_channel\_configuration | [Read channel configuration](https://ethicsportal.eu/developers/api/operations/getchannelconfiguration/) | channel:write | | update\_channel\_configuration | [Configure a whistleblowing channel](https://ethicsportal.eu/developers/api/operations/updatechannelconfiguration/) | channel:write | | list\_webhooks | [List webhooks](https://ethicsportal.eu/developers/api/operations/listwebhooks/) | webhooks:read | | create\_webhook | [Create a webhook](https://ethicsportal.eu/developers/api/operations/createwebhook/) | webhooks:write | | get\_webhook | [Read a webhook](https://ethicsportal.eu/developers/api/operations/getwebhook/) | webhooks:read | | update\_webhook | [Update a webhook](https://ethicsportal.eu/developers/api/operations/updatewebhook/) | webhooks:write | | delete\_webhook | [Delete a webhook](https://ethicsportal.eu/developers/api/operations/deletewebhook/) | webhooks:write | | rotate\_webhook\_secret | [Rotate a webhook's signing secret](https://ethicsportal.eu/developers/api/operations/rotatewebhooksecret/) | webhooks:write | | test\_webhook | [Send a test event](https://ethicsportal.eu/developers/api/operations/testwebhook/) | webhooks:write | | list\_webhook\_deliveries | [List webhook deliveries](https://ethicsportal.eu/developers/api/operations/listwebhookdeliveries/) | webhooks:read | | retry\_webhook\_delivery | [Retry a failed delivery](https://ethicsportal.eu/developers/api/operations/retrywebhookdelivery/) | webhooks:write | | list\_audit\_trail | [List the audit trail](https://ethicsportal.eu/developers/api/operations/listaudittrail/) | audit\_log:read | | list\_report\_audit\_trail | [List a report's audit trail](https://ethicsportal.eu/developers/api/operations/listreportaudittrail/) | audit\_log:read | | list\_members | [List members](https://ethicsportal.eu/developers/api/operations/listmembers/) | members:read | | list\_invitations | [List pending invitations](https://ethicsportal.eu/developers/api/operations/listinvitations/) | members:read | | create\_invitation | [Invite a member](https://ethicsportal.eu/developers/api/operations/createinvitation/) | members:write | | revoke\_invitation | [Revoke an invitation](https://ethicsportal.eu/developers/api/operations/revokeinvitation/) | members:write | | resend\_invitation | [Resend an invitation](https://ethicsportal.eu/developers/api/operations/resendinvitation/) | members:write | | update\_organization | [Rename the organisation](https://ethicsportal.eu/developers/api/operations/updateorganization/) | organizations:write | | list\_documents | [List documents](https://ethicsportal.eu/developers/api/operations/listdocuments/) | documents:read | | get\_billing\_status | [Read billing status](https://ethicsportal.eu/developers/api/operations/getbillingstatus/) | billing:read | | create\_checkout\_link | [Create a subscription checkout link](https://ethicsportal.eu/developers/api/operations/createcheckoutlink/) | billing:write | | create\_billing\_portal\_link | [Create a billing portal link](https://ethicsportal.eu/developers/api/operations/createbillingportallink/) | billing:write | | submit\_feedback | [Send product feedback](https://ethicsportal.eu/developers/api/operations/submitfeedback/) | None | ### Arguments * **Path parameters**, such as `organization_id`, are record IDs. Take them from `get_identity` and from earlier results. A value that is not a record ID is refused before anything runs. * **Query parameters**, such as `limit`, `cursor` and `status`, are arguments with the same names. * **The request body** keeps its shape. `create_webhook` takes a `webhook` object, as the API does. * **`idempotency_key`** is an argument of every tool whose operation accepts the `Idempotency-Key` header. * Arguments that the tool does not declare are refused. An `Idempotency-Key` header that the client sends to the MCP endpoint does not reach the operation: use the argument. Conditional requests with `If-None-Match` are not available through tools. The `params` of a `tools/call` request: ``` { "name": "list_reports", "arguments": { "organization_id": "gHiJkl", "status": "received", "limit": 50 } } ``` ### Results and errors A successful call returns the JSON response of the API in `structuredContent`. The same JSON is in `content` as text. An operation that returns no content answers with the text `Done.` When the API refuses the call, the result has `isError: true`, and `structuredContent` holds the [error body of the API](https://ethicsportal.eu/developers/pagination-and-errors/#errors). The HTTP status of the MCP response is still `200`, so read `error.code`: ``` { "content": [ { "type": "text", "text": "{\"error\":{\"code\":\"validation_failed\",\"fields\":[\"url\"],\"feedback_url\":\"https://secure.ethicsportal.eu/me/feedback.json\"}}" } ], "structuredContent": { "error": { "code": "validation_failed", "fields": ["url"], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } }, "isError": true } ``` `feedback_url` is the address of the product feedback operation. Through MCP, use the `submit_feedback` tool for the same purpose. Through a connection, `get_identity` returns `type` `connection`, the name of the app as `name`, and `last_four` `null`. Its `expires_at` is the end of the connection, and `organizations` lists only the organisations the connection reaches. ### Pagination, retries and rate limits * **Pagination.** A list returns `next_cursor`. Call the tool again with `cursor` set to that value until it is `null`. [Pagination](https://ethicsportal.eu/developers/pagination-and-errors/#pagination). * **Safe retries.** Send `idempotency_key` with every tool that accepts it, and send the same key again when you retry. The rules are those of the API: [Safe retries](https://ethicsportal.eu/developers/pagination-and-errors/#safe-retries). Keys belong to the connection. A new approval creates a new connection, so earlier keys do not carry over. * **Rate limits of the endpoint.** Each connection can send 120 MCP requests per minute. Requests without a usable token are limited to 120 per minute per source address. Above the limit the HTTP response is `429` with `Retry-After: 60`. * **Rate limits of the API.** Tool calls also count toward the [API limits](https://ethicsportal.eu/developers/pagination-and-errors/#rate-limits): 60 per minute per connection and 300 per minute per organisation. Above these, the tool result is an error with the code `rate_limited`. ### Operations without a tool | API operation | Why it has no tool | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | [List my invitations](https://ethicsportal.eu/developers/api/operations/listmyinvitations/) | It needs invitations:respond, which no connection can hold. | | [Accept an invitation](https://ethicsportal.eu/developers/api/operations/acceptinvitation/) | It needs invitations:respond, which no connection can hold. | | [Decline an invitation](https://ethicsportal.eu/developers/api/operations/declineinvitation/) | It needs invitations:respond, which no connection can hold. | | [Download the compliance report](https://ethicsportal.eu/developers/api/operations/downloadcompliancereport/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | | [Download the compliance certificate](https://ethicsportal.eu/developers/api/operations/downloadcompliancecertificate/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | | [Download the whistleblowing policy](https://ethicsportal.eu/developers/api/operations/downloadwhistleblowerpolicy/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | | [Download the privacy notice](https://ethicsportal.eu/developers/api/operations/downloadprivacynotice/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | | [Download the poster](https://ethicsportal.eu/developers/api/operations/downloadposter/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | | [Download the case handler manual](https://ethicsportal.eu/developers/api/operations/downloadcasehandlermanual/) | It returns a file, and a file does not fit a tool result. list\_documents returns its download link. | `list_documents` returns the address of each document. An access token of a connection cannot download it, because the API refuses that token. A signed-in person can open the address in a browser, and a script can download it with an [API token](https://ethicsportal.eu/developers/authentication/). Billing tools return a link to a Stripe-hosted page for a person to open. They never take a payment. ## Data boundary The MCP server has the same [data boundary](https://ethicsportal.eu/developers/data-boundaries/) as the API. Report content, reporter identity, messages, notes and attachments are never available. No tool reads report content, and no tool submits or changes a report. A tool returns the response of its API operation, so every field that the API excludes is also absent here. Excluded fields are omitted entirely. They are never returned as `REDACTED`, and there is no expansion option. No tool returns report content. One permission can give another person access to reports in the web application: see `members:write` under [Scopes](https://ethicsportal.eu/developers/mcp/#scopes). Operational metadata can still identify a case inside your organisation. A tool result leaves EthicsPortal and goes to the assistant that the person connected. Administrators can see and remove these connections under **Connected apps**. ## Manage connections * **The person** sees their connections in account settings under **Connected apps**. Each entry shows the app, its address, the organisations, the permissions, the dates and the last activity. **Disconnect** ends the connection in every organisation. * **An administrator** sees every connection that reaches the organisation under Organisation → Integrations → **Connected apps**, with the member who connected it. **Remove access** removes that organisation from the connection. The person’s other organisations are not affected. * **The audit trail.** Where the API records a request in the audit trail, it records the same tool call. The entry names the person and is marked **AI assistant**, with the name and address of the app. In the [audit trail API](https://ethicsportal.eu/developers/administration/#read-the-audit-trail), the actor’s kind is `mcp`. Approving, removing and ending a connection is recorded in the audit trail of each organisation it reaches. ## Errors and troubleshooting HTTP responses of the MCP endpoint: | Status | Meaning | Action | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | 400 | The body is not one JSON-RPC request, or the protocol revision is not supported | Send one request per POST. The error lists the supported revisions | | 401 | There is no token, or the token cannot be used: it is unknown, expired or issued for another resource, or its connection has ended | Refresh the access token. If the refresh answers invalid\_grant, start a new authorization request | | 403 | The tool needs a scope the connection does not have | [Ask for a further scope](https://ethicsportal.eu/developers/mcp/#ask-for-a-further-scope) | | 404 | Assistant connections are switched off. In the 2026-07-28 lifecycle, also a JSON-RPC method that the server does not offer, such as prompts/list | Try again later, or write to support. Use tools only | | 405 | The method is GET or DELETE. The response has Allow: POST | Send POST | | 406, 415 | The Accept or Content-Type header is not accepted | Send the headers shown under [Endpoint and transport](https://ethicsportal.eu/developers/mcp/#endpoint-and-transport) | | 413 | The request body is larger than 1 MB | Send a smaller request | | 429 | The connection or the source address sent too many requests | Wait for the Retry-After interval | A tool error arrives with HTTP `200` and `isError: true`. The codes are those of the [API errors](https://ethicsportal.eu/developers/pagination-and-errors/#errors). The most frequent: | Code | Meaning | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | not\_found | The record does not exist, the person cannot access it, the organisation is not part of the connection, or API access is suspended | | forbidden | The person’s current role does not allow the action | | agreement\_required | The person or the organisation must accept an agreement in the web application | | validation\_failed | Correct the fields named in fields | | rate\_limited | Wait one minute | OAuth endpoints: | Where | Answer | Meaning | | ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Registration | 400 invalid\_redirect\_uri | A redirect address is missing or not accepted | | Registration | 400 invalid\_client\_metadata | The client is not a public client, or a field is not accepted | | Registration, token, revocation | 413 | The request body is larger than 16 KB | | Authorization | Error page | The request is not valid. The browser is not sent back to the client | | Authorization | Page “Assistant connections are not available” | The person has no organisation with API access, and the request does not include organizations:create | | Token | invalid\_grant | The code or refresh token is unknown, expired or already used, the PKCE verifier is wrong, or the connection has ended | | Token | invalid\_target | resource is not the MCP endpoint | | Token | invalid\_scope | A refresh asked for scopes other than those of the connection | | Any | 429 | Too many requests. Registration allows 20 per hour and authorization 30 per minute per source address. The token and revocation endpoints allow 60 per minute per client at one source address, and 600 per minute per source address | --- # SAML and SCIM Source: https://ethicsportal.eu/developers/identity-integrations/ EthicsPortal supports two identity integrations. Each one is configured per organisation and works without the other: * **SAML 2.0 single sign-on** lets members sign in through your identity provider. * **SCIM 2.0 provisioning** lets your identity provider add, update and deactivate members. Use both to keep access to cases in step with your directory: SCIM decides who is a member, and SAML decides how they sign in. ## Before you start | Requirement | Details | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Role | Organisation **Admin**. Other roles do not see these settings. | | Subscription | The organisation needs active access. Without it, the settings open the billing page instead. | | Where | **Organisation → Enterprise identity → Single sign-on (SSO)** and **Organisation → Enterprise identity → SCIM provisioning** | | Identity check | EthicsPortal asks you to confirm your identity before it opens either screen. | | Credentials | SAML uses your identity provider’s signing certificate. SCIM uses its own bearer token. Neither uses an [API token](https://ethicsportal.eu/developers/authentication/), and an API token does not work for either. | All values on this page use the production host `https://secure.ethicsportal.eu`. The settings screens show the same values with a copy button. ## SAML 2.0 single sign-on EthicsPortal acts as the SAML service provider (SP). One set of SP values serves every organisation: EthicsPortal finds the right organisation from the email domain the person enters, so there is no organisation-specific URL. | Setting | Value | | -------------------------- | ------------------------------------------------- | | SP entity ID and metadata | https://secure.ethicsportal.eu/auth/saml/metadata | | Assertion consumer service | https://secure.ethicsportal.eu/auth/saml/callback | | Binding | HTTP-POST | | NameID | Email address | The metadata URL returns the SP metadata as XML. You can import it in identity providers that accept a metadata URL, or enter the two URLs by hand. The metadata states that assertions must be signed and requests the `email` and `name` attributes. ### Identity provider requirements | Item | Requirement | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Sign-in flow | Service-provider-initiated only. People start at EthicsPortal. Sign-in started from an identity provider dashboard or app tile is refused. | | Authentication request | EthicsPortal sends an unsigned request to your SSO URL with the HTTP-Redirect binding. Do not require signed requests. | | Response | Sent to the assertion consumer service URL with HTTP-POST. The Destination must be that URL. | | Audience | Exactly the SP entity ID: https://secure.ethicsportal.eu/auth/saml/metadata | | Issuer | Exactly the identity provider entity ID you enter in EthicsPortal | | Signature | The **assertion** must be signed. A response where only the outer envelope is signed is refused. Signing both is fine. | | Algorithms | RSA-SHA256 signature and SHA-256 digest. SHA-1 is refused. | | Encryption | Do not encrypt the assertion. The connection is protected by HTTPS. | | Validity | The assertion must be inside its validity window. EthicsPortal allows 30 seconds of clock difference. | | Time limit | The person must finish signing in within 10 minutes of starting at EthicsPortal. | | Single logout | Not supported. Leave the single logout URL empty. | ### Attributes | Data | Where EthicsPortal reads it | Required | | ------------- | -------------------------------------------------------------------------------------------------- | -------- | | Email address | An attribute named email or mail. If neither is present, the NameID value. | Yes | | NameID format | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress, with the user’s email address as the value | Yes | | Display name | An attribute named name | No | Attribute names are matched exactly. Names written as URIs, or `first_name` and `last_name`, are not read. Without a `name` attribute, a person created on first sign-in is named after their email address. The email address must be in one of the organisation’s verified domains. EthicsPortal compares addresses without regard to letter case. ### Set up single sign-on 1. In your identity provider, create a SAML 2.0 application for EthicsPortal. Enter the SP entity ID and the assertion consumer service URL from the table above. Set the NameID to the user’s email address with the email address format, and add a `name` attribute if you want display names. 2. Make sure the application signs the assertion with SHA-256. 3. In EthicsPortal, open **Organisation → Enterprise identity → Single sign-on (SSO)**. Copy these values from your identity provider: * **Identity provider entity ID**: the issuer from your identity provider’s SAML metadata. * **Identity provider SSO URL**: the sign-on endpoint. It must be an `https://` URL. * **Identity provider certificate**: the X.509 signing certificate, in PEM format (with or without the `BEGIN CERTIFICATE` lines). 4. Choose the **Default role for provisioned members** and whether to **Provision members on first sign-in**. Leave **Enable single sign-on** and **Require single sign-on** off for now. Select **Save changes**. 5. Under **Email domains**, add each domain your people’s addresses use, then verify it. See [Verify your email domains](https://ethicsportal.eu/developers/identity-integrations/#verify-your-email-domains). 6. Turn on **Enable single sign-on** and save. EthicsPortal refuses to enable a connection without the three identity provider values and at least one verified domain. 7. Test it. Sign out, open the sign-in page, choose **Sign in with single sign-on** and enter your work email. You can also go straight to `https://secure.ethicsportal.eu/sso/new`. The settings screen then shows the time of the last sign-in through your identity provider. 8. When the test works, decide whether to turn on **Require single sign-on**. See [Require single sign-on](https://ethicsportal.eu/developers/identity-integrations/#require-single-sign-on). Assign the EthicsPortal application in your identity provider to the people who handle cases. EthicsPortal accepts an assertion only for an address in a verified domain. The [just-in-time provisioning](https://ethicsportal.eu/developers/identity-integrations/#just-in-time-provisioning) setting and existing memberships decide what that person can then reach. ### Verify your email domains A domain does nothing until it is verified. Only verified domains send people to your identity provider, and only addresses in verified domains are accepted from it. 1. Enter the domain under **Email domains**, for example `example.com`, and select **Add domain**. 2. Publish the TXT record shown for it at the root (`@`) of that domain. It has this form: ``` ethicsportal-verification= ``` 3. Select **Verify**. EthicsPortal looks the record up when you press the button. If DNS has not updated yet, wait and try again; changes can take up to 24 hours. Each domain matches exactly. Add subdomains separately: verifying `example.com` does not cover `eu.example.com`. One connection can hold several domains. Only one EthicsPortal organisation can hold a domain as verified. Once verified, EthicsPortal treats your identity provider as the sign-in route for every EthicsPortal account with an address in that domain. This includes accounts that belong only to other EthicsPortal organisations. The identity provider signs those people in, but that does not add them to your organisation. Removing a domain stops sending its addresses to your identity provider. ### Just-in-time provisioning **Provision members on first sign-in** is off by default. Leave it off if you add members with SCIM or invitations. | Person signing in | Provisioning off | Provisioning on | | ----------------------------------------------- | ----------------------------------------- | ---------------------------------------------------- | | Active member of your organisation | Signed in | Signed in | | Deactivated member of your organisation | Signed in, no access to your organisation | Reactivated, then signed in | | Has no EthicsPortal account | Refused | Account and membership created with the default role | | Has an EthicsPortal account but is not a member | Signed in, not added to your organisation | Signed in, not added to your organisation | People who already hold an EthicsPortal account join your organisation by accepting an invitation, sent by an administrator or by [SCIM](https://ethicsportal.eu/developers/identity-integrations/#scim-20-provisioning). Single sign-on alone never adds an existing account to an organisation. A reactivated member returns with the lower of their previous role and the default role. The identity provider confirms who someone is. It does not restore a role an administrator removed. If a member has turned on two-factor authentication in EthicsPortal, they still enter their authenticator code after signing in through your identity provider. ### Require single sign-on **Require single sign-on** applies to every address in the connection’s verified domains: * Emailed sign-in codes (magic links) are refused for those addresses. The sign-in page sends them to single sign-on with the message “Your organization requires single sign-on. Continue with your identity provider.” * Existing sessions for those addresses that were not started through your identity provider end at their next request. This also happens when you enable an enforced connection or verify another domain on it. * A member with an address in an enforced domain can change their email only to another address in the same domains. Nobody can change their email into a domain where another organisation requires single sign-on unless they are a member of that organisation. To prevent a lockout, an administrator whose own address is in the domain must be signed in through single sign-on before turning this on. Otherwise saving fails with “Sign in through single sign-on once before requiring it, so a setup mistake can’t lock you out.” Addresses outside the verified domains, such as external counsel, keep signing in with emailed codes. ### Sessions and sign-out An EthicsPortal session ends after 14 days without activity, or when the person signs out. Signing out of your identity provider, or disabling the person there, does not end an EthicsPortal session that already exists. To remove access at once, deactivate the member: through SCIM, or in **Team** in EthicsPortal. ### Rotate the signing certificate Identity providers issue a new signing certificate before the old one expires. To rotate without interruption: 1. Paste the new certificate below the old one in **Identity provider certificate** and save. Assertions signed with either certificate are accepted. 2. Switch your identity provider to the new certificate. 3. Remove the old certificate from the field and save. The settings screen shows the latest expiry date among the certificates in the field. When every certificate has expired, it shows a warning and all sign-ins fail until you add a valid one. ### Okta | Okta field | Value | | --------------------------- | ------------------------------------------------------------------------------------------------ | | Single sign-on URL | https://secure.ethicsportal.eu/auth/saml/callback (keep it as the recipient and destination URL) | | Audience URI (SP Entity ID) | https://secure.ethicsportal.eu/auth/saml/metadata | | Name ID format | EmailAddress | | Application username | Email | | Attribute statements | email \= user.email; name \= user.firstName + " " + user.lastName | | Assertion signature | Signed, RSA-SHA256 | Copy Okta’s **Identity Provider Issuer**, **Identity Provider Single Sign-On URL** and **X.509 Certificate** into the matching EthicsPortal fields. Sign-in from the Okta dashboard tile is identity-provider-initiated and is refused. Hide the tile, or replace it with a bookmark to `https://secure.ethicsportal.eu/sso/new`. ### Microsoft Entra ID | Entra ID field | Value | | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | | Identifier (Entity ID) | https://secure.ethicsportal.eu/auth/saml/metadata | | Reply URL (Assertion Consumer Service URL) | https://secure.ethicsportal.eu/auth/saml/callback | | Sign on URL | https://secure.ethicsportal.eu/sso/new, so that opening the app from My Apps starts at EthicsPortal | | Unique User Identifier (Name ID) | The user’s email address, for example user.mail, with the email address format | | Additional claim | Name name with no namespace, source user.displayname | | Signing option | Sign SAML assertion, or sign both the response and the assertion | | Signing algorithm | SHA-256 | Copy **Microsoft Entra Identifier** to **Identity provider entity ID**, **Login URL** to **Identity provider SSO URL**, and the contents of **Certificate (Base64)** to **Identity provider certificate**. Entra ID’s default claims use URI names, which EthicsPortal does not read. The email address therefore comes from the NameID. If a user principal name differs from the email address, map the NameID to the email address. ### Google Workspace and other identity providers Any SAML 2.0 identity provider that meets the [requirements](https://ethicsportal.eu/developers/identity-integrations/#identity-provider-requirements) works. In a custom SAML application, enter the assertion consumer service URL as the ACS URL and the SP entity ID as the entity ID. Set the NameID format to email, with the user’s primary email as the value. Add a `name` attribute for display names. ## SCIM 2.0 provisioning | Setting | Value | | -------------- | -------------------------------------------------------------- | | Base URL | https://secure.ethicsportal.eu/scim/v2 | | Authentication | Dedicated bearer token generated in organisation SCIM settings | | Resource | /Users | | Media type | application/scim+json | The token identifies the organisation, so the base URL is the same for every organisation. A SCIM user is a person’s membership in your organisation. ### Generate and manage the token 1. Open **Organisation → Enterprise identity → SCIM provisioning** and select **Generate token**. 2. Copy the token. It starts with `scim_` and is shown only once. EthicsPortal cannot display it again. 3. In your identity provider, enter the base URL and the token as the bearer token or secret. | Action | Effect | | ---------------------------------------- | --------------------------------------------------------------------------------- | | **Regenerate token** | Issues a new token. The old token stops working immediately. | | **Disable** | Every request is refused with 401 until you select **Enable**. The token is kept. | | **Remove** | Deletes the connection and its token. Members already provisioned stay. | | **Default role for provisioned members** | The role new members receive: Admin, Handler or Viewer. Handler is the default. | The screen shows the last four characters of the token and the time of the last request from your identity provider. Send the token in the `Authorization` header on every request: ``` Authorization: Bearer scim_... Content-Type: application/scim+json ``` ### Endpoints | Method | Path | Purpose | | ------ | ------------------------------------- | --------------------------------------------- | | GET | /Users | List members, with optional filter and paging | | GET | /Users/{id} | Read one member | | POST | /Users | Provision a member | | PUT | /Users/{id} | Replace a member’s attributes | | PATCH | /Users/{id} | Update attributes or deactivate | | DELETE | /Users/{id} | Deactivate a member | | GET | /ServiceProviderConfig | Supported features | | GET | /ResourceTypes, /ResourceTypes/{name} | Resource types | | GET | /Schemas | Schemas | All endpoints require the token. There is no `/Groups` endpoint and no bulk endpoint. Turn off group push or group provisioning in your identity provider; assigning a group to the application still provisions its members one by one. Sorting and ETags are not supported. ### Attribute mapping | SCIM attribute | Read and write | What it sets in EthicsPortal | | ------------------------------- | ----------------------- | ------------------------------------------------------------------ | | id | Read only | EthicsPortal’s identifier for the member. Use it in /Users/{id}. | | externalId | Read and write | Your identity provider’s identifier, stored and returned unchanged | | userName | Read; written on create | The person’s email address. It must be an email address. | | emails\[primary eq true\].value | Read; written on create | The same email address, returned with type work | | displayName | Read and write | The person’s name in your organisation | | name.formatted | Read and write | Same as displayName | | active | Read and write | false deactivates the member, true reactivates them | | meta.created, meta.lastModified | Read only | When the member was added and last changed | Other attributes, including `name.givenName`, `name.familyName` and `roles`, are not stored. Map `displayName` if you want names to appear. The email address is fixed once a member exists. A later change to `userName` or `emails` is accepted but has no effect. An EthicsPortal account can belong to several organisations, and one organisation’s identity provider cannot change it for the others. A name set through SCIM applies in your organisation only. When SCIM creates a brand-new account, the name also becomes that account’s name. Roles are not synchronised. New members receive the default role, and administrators change roles in EthicsPortal. ### Provision a user ``` POST /scim/v2/Users HTTP/1.1 Host: secure.ethicsportal.eu Authorization: Bearer scim_... Content-Type: application/scim+json { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "jane.doe@example.com", "externalId": "00u1abcd2EFGH3ijk4l5", "displayName": "Jane Doe", "emails": [{ "value": "jane.doe@example.com", "type": "work", "primary": true }], "active": true } ``` Response `201 Created`: ``` { "id": "4821", "externalId": "00u1abcd2EFGH3ijk4l5", "userName": "jane.doe@example.com", "name": { "formatted": "Jane Doe" }, "displayName": "Jane Doe", "emails": [{ "primary": true, "value": "jane.doe@example.com", "type": "work" }], "active": true, "meta": { "location": "https://secure.ethicsportal.eu/scim/v2/Users/4821", "created": "2026-11-09T10:15:00Z", "lastModified": "2026-11-09T10:15:00Z", "resourceType": "User" }, "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"] } ``` What `POST /Users` does depends on the address: | Address | Result | | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No EthicsPortal account | Account and active membership created with the default role. 201. | | Deactivated member of your organisation | Reactivated with the lower of their previous role and the default role. 201. | | Active member of your organisation | 409, scimType uniqueness | | Existing EthicsPortal account, not a member | Not added. EthicsPortal emails the person an invitation and answers 409. After they accept, retry the assignment: the identity provider finds the member by userName. | | In a domain another organisation has verified for single sign-on | 409. Invite the person instead. | An account that was never confirmed and belongs to no organisation counts as having no account if your organisation has verified its domain for single sign-on. Repeated requests for the same address do not send duplicate invitations while one is pending. New members receive no welcome email from provisioning. They sign in through single sign-on, or with an emailed code if single sign-on is not required for their domain. ### List and filter users ``` GET /scim/v2/Users?filter=userName%20eq%20%22jane.doe%40example.com%22 HTTP/1.1 ``` ``` { "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "totalResults": 1, "startIndex": 1, "itemsPerPage": 100, "Resources": [{ "id": "4821", "userName": "jane.doe@example.com", "active": true }] } ``` The list contains every member of your organisation, including members added by invitation and deactivated members (`"active": false`). The `Resources` entries are full user objects, shortened here. | Filter attribute | Notes | | -------------------- | ----------------------------------------- | | userName | Matched without regard to letter case | | emails, emails.value | Same as userName | | externalId | Case-sensitive | | id | Case-sensitive | | active | Accepted, but does not narrow the results | Supported operators: `eq`, `ne`, `co`, `sw`, `ew`, `gt`, `ge`, `lt`, `le`, `pr`, combined with `and` and `or`. Filtering on any other attribute returns `400` with `scimType` `invalidFilter`. ### Pagination | Parameter | Default | Meaning | | ---------- | ------- | ------------------------------------------------------- | | startIndex | 1 | 1-based position of the first result | | count | 100 | Maximum number of results. 0 returns only totalResults. | Results are ordered by `id`, oldest first. `itemsPerPage` repeats the requested `count`. Continue until `startIndex + count` is greater than `totalResults`. ### Update and replace users `PATCH` accepts a SCIM `PatchOp` with the operations `add`, `replace` and `remove`, with or without a `path`: ``` { "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [ { "op": "replace", "path": "displayName", "value": "Jane Smith" }, { "op": "replace", "value": { "active": false } } ] } ``` Send `active` as a JSON boolean (`true` or `false`). `PUT` replaces the member’s attributes with the request body. Send the full user, including `externalId` and `active`. A `PUT` that leaves out `active` does not deactivate anyone. Both return `200` with the updated user. ### Deactivate users `DELETE /Users/{id}`, or setting `active` to `false`, deactivates the membership. It does not delete it. `DELETE` returns `204 No Content` and is safe to repeat. When a member is deactivated: * They lose access to the organisation at their next request. * Their open reports are unassigned and they are removed from reports they participate in. * They see a notification in EthicsPortal that their access ended. * The change is recorded in the audit trail as made by SCIM. * Their past actions stay attributed to them in case history and the audit trail. Some members cannot be deactivated. EthicsPortal answers `409` with the reason, and the member stays active: | Member | Reason returned | What to do | | ------------------------------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Organisation owner | The organization owner cannot be deactivated. | Transfer ownership in EthicsPortal first | | Only active admin | This is the only active admin. | Make another member an Admin first | | Compliance officer of the whistleblowing channel | This member is the whistleblowing channel’s compliance officer. | Assign a different compliance officer in the whistleblowing channel settings first | Setting `active` to `true`, or a new `POST` for the same address, reactivates the member with the lower of their previous role and the default role. ### SCIM errors Errors use the SCIM error format: ``` { "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "detail": "User jane.doe@example.com is already provisioned in this organization.", "status": "409", "scimType": "uniqueness" } ``` | Status | Cause | | ------ | ---------------------------------------------------------------------------------------------------------- | | 400 | Invalid JSON, empty body, invalid resource, unknown PATCH operation, or unsupported filter (invalidFilter) | | 401 | Missing or unknown token, a regenerated token, or a disabled or removed connection | | 404 | No member with that id in this organisation | | 406 | Unsupported media type. Send Content-Type: application/scim+json. | | 409 | See [Provision a user](https://ethicsportal.eu/developers/identity-integrations/#provision-a-user) and [Deactivate users](https://ethicsportal.eu/developers/identity-integrations/#deactivate-users) | | 429 | More than 300 requests in one minute with the same token. Retry after a pause. | ### Okta provisioning In the Okta application’s provisioning settings, use SCIM 2.0 with: | Okta field | Value | | --------------------------------- | --------------------------------------------------------------- | | SCIM connector base URL | https://secure.ethicsportal.eu/scim/v2 | | Unique identifier field for users | userName | | Supported provisioning actions | Push New Users, Push Profile Updates. Do not select group push. | | Authentication mode | HTTP Header, with the SCIM token as the bearer token | Then enable creating users, updating user attributes and deactivating users. Make sure `userName` is the user’s email address and `displayName` is mapped. ### Microsoft Entra ID provisioning Set the provisioning mode to automatic and enter: | Entra ID field | Value | | -------------- | -------------------------------------- | | Tenant URL | https://secure.ethicsportal.eu/scim/v2 | | Secret token | The SCIM token | Test the connection, then check the user attribute mappings. `userName` must hold the email address; if user principal names differ from email addresses, map `userName` to `mail`. Turn off group provisioning, because EthicsPortal has no `/Groups` endpoint. ## Troubleshooting ### Single sign-on messages | Message or symptom | Cause | Fix | | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | “Single sign-on isn’t set up for that email domain.” | The connection is not enabled, or the address’s domain is not verified on it | Enable the connection, and add and verify the domain | | “We couldn’t complete single sign-on. Please try again.” | The assertion was refused: wrong certificate, only the response signed, SHA-1, audience or issuer mismatch, wrong Destination, clock difference beyond 30 seconds, sign-in started from an identity provider tile, more than 10 minutes since starting, or the same response sent twice | Check the values in [identity provider requirements](https://ethicsportal.eu/developers/identity-integrations/#identity-provider-requirements) and start again at https://secure.ethicsportal.eu/sso/new | | “Your identity provider signed you in, but you don’t have access here yet. Ask an administrator to add you.” | The asserted email address is not in a verified domain, the person has no account and just-in-time provisioning is off, or the account cannot be used | Check the email the identity provider sends, invite the person or provision them with SCIM, or turn on just-in-time provisioning | | “needs at least one verified email domain before single sign-on can be enabled” | No verified domain | Verify a domain first | | “Sign in through single sign-on once before requiring it, so a setup mistake can’t lock you out.” | Your current session did not start through single sign-on | Sign out, sign in through single sign-on, then turn on **Require single sign-on** | | “doesn’t look like a valid X.509 certificate” | The certificate field contains something other than certificates | Paste the certificate from your identity provider, in PEM or Base64 form | | “We couldn’t find the TXT record yet” | The record is not visible in DNS yet, or is not at the domain root | Check the record and wait for DNS to update. | | “Another organisation has already verified this domain” | The domain is verified by a different organisation, and only one can hold it | If the domain is yours, [contact support](mailto:support@ethicsportal.eu). | | “Your organization requires single sign-on. Continue with your identity provider.” | Emailed sign-in codes are refused for this domain | Sign in through single sign-on | | All sign-ins fail from one day | The identity provider certificate expired or was replaced | Paste the current certificate. See [Rotate the signing certificate](https://ethicsportal.eu/developers/identity-integrations/#rotate-the-signing-certificate). | ### Provisioning problems | Symptom | Cause | Fix | | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | 401 on every request | Wrong token, token regenerated, or connection disabled | Enter the current token, or select **Enable** | | 409 with “an invitation to join this organization has been sent” | The address already has an EthicsPortal account | Ask the person to accept the invitation, then retry the assignment | | 409 with “is in a domain another organization has verified for single sign-on” | Another organisation owns that domain | Invite the person instead | | 409 when deactivating | The member is the owner, the only active admin, or the channel’s compliance officer | Follow the table in [Deactivate users](https://ethicsportal.eu/developers/identity-integrations/#deactivate-users) | | Members have no name | displayName is not mapped | Map displayName in your identity provider | | Email change in the directory not reflected | Email addresses are fixed once provisioned | The person changes their email in their EthicsPortal account settings | ## Metadata API and member lifecycle Deactivation through SCIM ends a member’s access to the organisation through the metadata API as well. Their personal API tokens stop reaching the organisation, and they stop receiving the alerts sent to administrators when a webhook fails. Organisation API tokens and webhooks belong to the organisation and keep working. [API permissions](https://ethicsportal.eu/developers/authentication/). The metadata API lists members and sends invitations, but it does not deactivate members or change roles. Use SCIM or the web application for that. See [Members and invitations](https://ethicsportal.eu/developers/administration/#members-and-invitations). For identity-provider configuration assistance, [contact support](mailto:support@ethicsportal.eu). --- # API reference Source: https://ethicsportal.eu/developers/api/ ## EthicsPortal API 1.1.0 Report metadata, organisation and whistleblowing channel setup, administration, product feedback and outbound webhooks. Each token carries the scopes chosen at issuance; write scopes are opt-in. Report content, attachments and their metadata, messages, notes, category, closure outcome, retaliation, reporter identity and access codes are always excluded. Billing and agreement acceptance require a browser session. Information * OpenAPI version: `3.1.0` ## Operations GET [/identity.json](https://ethicsportal.eu/developers/api/operations/getidentity/) GET [/organizations/{organization\_id}/whistleblower/portal.json](https://ethicsportal.eu/developers/api/operations/getchannel/) GET [/organizations/{organization\_id}/whistleblower/reports.json](https://ethicsportal.eu/developers/api/operations/listreports/) GET [/organizations/{organization\_id}/whistleblower/reports/{id}.json](https://ethicsportal.eu/developers/api/operations/getreport/) POST [/organizations.json](https://ethicsportal.eu/developers/api/operations/createorganization/) GET [/organizations/{organization\_id}/whistleblower/portal/configuration.json](https://ethicsportal.eu/developers/api/operations/getchannelconfiguration/) PATCH [/organizations/{organization\_id}/whistleblower/portal/configuration.json](https://ethicsportal.eu/developers/api/operations/updatechannelconfiguration/) GET [/organizations/{organization\_id}/webhooks.json](https://ethicsportal.eu/developers/api/operations/listwebhooks/) POST [/organizations/{organization\_id}/webhooks.json](https://ethicsportal.eu/developers/api/operations/createwebhook/) GET [/organizations/{organization\_id}/webhooks/{webhook\_id}.json](https://ethicsportal.eu/developers/api/operations/getwebhook/) DELETE [/organizations/{organization\_id}/webhooks/{webhook\_id}.json](https://ethicsportal.eu/developers/api/operations/deletewebhook/) PATCH [/organizations/{organization\_id}/webhooks/{webhook\_id}.json](https://ethicsportal.eu/developers/api/operations/updatewebhook/) POST [/organizations/{organization\_id}/webhooks/{webhook\_id}/rotate.json](https://ethicsportal.eu/developers/api/operations/rotatewebhooksecret/) POST [/organizations/{organization\_id}/webhooks/{webhook\_id}/test.json](https://ethicsportal.eu/developers/api/operations/testwebhook/) GET [/organizations/{organization\_id}/webhooks/{webhook\_id}/deliveries.json](https://ethicsportal.eu/developers/api/operations/listwebhookdeliveries/) POST [/organizations/{organization\_id}/webhooks/{webhook\_id}/deliveries/{delivery\_id}/retry.json](https://ethicsportal.eu/developers/api/operations/retrywebhookdelivery/) GET [/organizations/{organization\_id}/whistleblower/audit\_logs.json](https://ethicsportal.eu/developers/api/operations/listaudittrail/) GET [/organizations/{organization\_id}/whistleblower/reports/{id}/audit-log.json](https://ethicsportal.eu/developers/api/operations/listreportaudittrail/) GET [/organizations/{organization\_id}/members.json](https://ethicsportal.eu/developers/api/operations/listmembers/) GET [/organizations/{organization\_id}/invitations.json](https://ethicsportal.eu/developers/api/operations/listinvitations/) POST [/organizations/{organization\_id}/invitations.json](https://ethicsportal.eu/developers/api/operations/createinvitation/) DELETE [/organizations/{organization\_id}/invitations/{invitation\_id}.json](https://ethicsportal.eu/developers/api/operations/revokeinvitation/) POST [/organizations/{organization\_id}/invitations/{invitation\_id}/resend.json](https://ethicsportal.eu/developers/api/operations/resendinvitation/) PATCH [/organizations/{organization\_id}/profile.json](https://ethicsportal.eu/developers/api/operations/updateorganization/) GET [/me/invitations.json](https://ethicsportal.eu/developers/api/operations/listmyinvitations/) PATCH [/me/invitations/{invitation\_id}/accept.json](https://ethicsportal.eu/developers/api/operations/acceptinvitation/) PATCH [/me/invitations/{invitation\_id}/decline.json](https://ethicsportal.eu/developers/api/operations/declineinvitation/) GET [/organizations/{organization\_id}/whistleblower/documents.json](https://ethicsportal.eu/developers/api/operations/listdocuments/) GET [/organizations/{organization\_id}/whistleblower/documents/compliance\_report.pdf](https://ethicsportal.eu/developers/api/operations/downloadcompliancereport/) GET [/organizations/{organization\_id}/whistleblower/documents/compliance\_certificate.pdf](https://ethicsportal.eu/developers/api/operations/downloadcompliancecertificate/) GET [/organizations/{organization\_id}/whistleblower/documents/whistleblower\_policy.docx](https://ethicsportal.eu/developers/api/operations/downloadwhistleblowerpolicy/) GET [/organizations/{organization\_id}/whistleblower/documents/privacy\_notice.docx](https://ethicsportal.eu/developers/api/operations/downloadprivacynotice/) GET [/organizations/{organization\_id}/whistleblower/documents/poster.pdf](https://ethicsportal.eu/developers/api/operations/downloadposter/) GET [/organizations/{organization\_id}/whistleblower/documents/case\_handler\_manual.pdf](https://ethicsportal.eu/developers/api/operations/downloadcasehandlermanual/) GET [/organizations/{organization\_id}/subscriptions.json](https://ethicsportal.eu/developers/api/operations/getbillingstatus/) POST [/organizations/{organization\_id}/subscriptions/checkout.json](https://ethicsportal.eu/developers/api/operations/createcheckoutlink/) POST [/organizations/{organization\_id}/subscriptions/billing\_portal.json](https://ethicsportal.eu/developers/api/operations/createbillingportallink/) POST [/me/feedback.json](https://ethicsportal.eu/developers/api/operations/submitfeedback/) POST [Receive a webhook event](https://ethicsportal.eu/developers/api/webhooks/receivereportevent/) WEBHOOK ## Authentication ### personalToken Personal token, prefixed ep\_pat\_. It belongs to a user and acts with that user’s current role in every organisation they belong to, so some operations also need a role. Only personal tokens can create organisations, and answer the user’s own invitations. Send it as a bearer token. It expires after 90 days and carries fixed scopes chosen at issuance; each operation lists the scope it requires. **Security scheme type:** http ### organizationToken Organisation token, prefixed ep\_oat\_. It belongs to one organisation, is managed by its administrators and keeps working when staff change. It has no role: its scopes alone decide what it can do. Send it as a bearer token. It expires after 90 days and carries fixed scopes chosen at issuance; each operation lists the scope it requires. **Security scheme type:** http --- # Discover organisations Source: https://ethicsportal.eu/developers/api/operations/getidentity/ GET /identity.json ``` const url = 'https://secure.ethicsportal.eu/identity.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/identity.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Return the organisations that an access token can read, with the token’s scopes and expiry date. Use it to find organisation IDs. An assistant connection of a person who has no organisation yet returns an empty list. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** ## Parameters ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Metadata only. Dates describe recorded workflow state, not proof of notification delivery. application/json object **type** required Personal: acts as its user, with that user’s current role, in each of their organisations. organization: belongs to one organisation and ignores roles; its scopes alone apply. string Allowed values: personal organization **name** required The name given to the token when it was created, as shown on the token list. string <= 100 characters **last\_four** required The token’s last four characters, as shown on the token list. string \>= 4 characters <= 4 characters **scopes** required Array <= 14 items unique items Allowed values: reports:read channel:read channel:write webhooks:read webhooks:write audit\_log:read members:read members:write documents:read billing:read billing:write organizations:write organizations:create invitations:respond **expires\_at** required string format: date-time **last\_used\_at** required When the token was last used before this request, recorded to the minute; null if this is its first use. string | null format: date-time **organizations** required Array <= 100 items object **id** required string **name** required string **reports\_url** required string format: uri **channel\_url** required string format: uri **members\_url** required string format: uri **webhooks\_url** required string format: uri **audit\_trail\_url** required string format: uri **documents\_url** required string format: uri **billing\_url** required string format: uri **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "type": "personal", "name": "Case dashboard", "last_four": "x7Qa", "scopes": [ "reports:read", "channel:read" ], "expires_at": "2027-02-05T10:00:00Z", "last_used_at": "2026-11-09T16:42:00Z", "organizations": [ { "id": "gHiJkl", "name": "Example organisation", "reports_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports.json", "channel_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/portal.json", "members_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/members.json", "webhooks_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks.json", "audit_trail_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/audit_logs.json", "documents_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/documents.json", "billing_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/subscriptions.json" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, invalid, expired or revoked bearer token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **WWW-Authenticate** string Bearer realm=“EthicsPortal” ### 403 Required agreement not accepted or permission denied application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "agreement_required", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Missing or inaccessible organisation or resource, or feature disabled application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **Retry-After** string 60 seconds --- # Read whistleblowing channel metadata Source: https://ethicsportal.eu/developers/api/operations/getchannel/ GET /organizations/{organization\_id}/whistleblower/portal.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/portal.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/portal.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Return the whistleblowing channel of one organisation: its ID, whether it is active and the URL of its online reporting form. Any member’s personal token can read it. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** channel:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** channel:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Metadata only. Dates describe recorded workflow state, not proof of notification delivery. application/json object **id** required string **active** required boolean **reporting\_form\_url** required string format: uri **configuration\_url** required string format: uri **documents\_url** required string format: uri #### Example ``` { "id": "QATEST12", "active": true, "reporting_form_url": "https://secure.ethicsportal.eu/p/QATEST12", "configuration_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/portal/configuration.json", "documents_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/documents.json" } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, invalid, expired or revoked bearer token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **WWW-Authenticate** string Bearer realm=“EthicsPortal” ### 403 Required agreement not accepted or permission denied application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "agreement_required", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Missing or inaccessible organisation or resource, or feature disabled application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **Retry-After** string 60 seconds --- # List report metadata Source: https://ethicsportal.eu/developers/api/operations/listreports/ GET /organizations/{organization\_id}/whistleblower/reports.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/reports.json?limit=50&status=received'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/whistleblower/reports.json?limit=50&status=received' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application List report metadata for one organisation: status, assignment, deadlines and overdue flags. Filter by status or creation date and page with a cursor. A personal token lists only the reports its user can open: every report for administrators and viewers, and for members only the reports assigned to them or in which they participate. An organisation token lists every report. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** reports:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** reports:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. **status** string Allowed values: received acknowledged feedback\_given closed **created\_after** string format: date-time Exclusive lower receipt-time bound. **created\_before** string format: date-time Exclusive upper receipt-time bound. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Metadata only. Dates describe recorded workflow state, not proof of notification delivery. application/json object **data** required Array <= 100 items object **id** required string **organization\_id** required string **status** required string Allowed values: received acknowledged feedback\_given closed **assigned** required boolean **created\_at** required string format: date-time **acknowledged\_at** required string | null format: date-time **feedback\_given\_at** required string | null format: date-time **closed\_at** required string | null format: date-time **acknowledgment\_due\_at** required string format: date-time **feedback\_due\_at** required string format: date-time **acknowledgment\_overdue** required boolean **feedback\_overdue** required boolean **url** required string format: uri **web\_url** required string format: uri **audit\_trail\_url** required string format: uri **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "aBcDef", "organization_id": "gHiJkl", "status": "received", "assigned": false, "created_at": "2026-11-07T10:00:00Z", "acknowledged_at": null, "feedback_given_at": null, "closed_at": null, "acknowledgment_due_at": "2026-11-14T10:00:00Z", "feedback_due_at": "2027-02-14T10:00:00Z", "acknowledgment_overdue": false, "feedback_overdue": false, "url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef.json", "web_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef", "audit_trail_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef/audit-log.json" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, invalid, expired or revoked bearer token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **WWW-Authenticate** string Bearer realm=“EthicsPortal” ### 403 Required agreement not accepted or permission denied application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "agreement_required", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Missing or inaccessible organisation or resource, or feature disabled application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **Retry-After** string 60 seconds --- # Read report metadata Source: https://ethicsportal.eu/developers/api/operations/getreport/ GET /organizations/{organization\_id}/whistleblower/reports/{id}.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/reports/example.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/reports/example.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Read the metadata of one report: status, assignment, acknowledgement and feedback deadlines, and overdue flags. Report content is never returned. A personal token reads only a report its user can open; any other report returns 404\. An organisation token reads every report. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** reports:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** reports:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **id** required string Obfuscated report ID; not the reporter access code. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Metadata only. Dates describe recorded workflow state, not proof of notification delivery. application/json object **id** required string **organization\_id** required string **status** required string Allowed values: received acknowledged feedback\_given closed **assigned** required boolean **created\_at** required string format: date-time **acknowledged\_at** required string | null format: date-time **feedback\_given\_at** required string | null format: date-time **closed\_at** required string | null format: date-time **acknowledgment\_due\_at** required string format: date-time **feedback\_due\_at** required string format: date-time **acknowledgment\_overdue** required boolean **feedback\_overdue** required boolean **url** required string format: uri **web\_url** required string format: uri **audit\_trail\_url** required string format: uri #### Example ``` { "id": "aBcDef", "organization_id": "gHiJkl", "status": "received", "assigned": false, "created_at": "2026-11-07T10:00:00Z", "acknowledged_at": null, "feedback_given_at": null, "closed_at": null, "acknowledgment_due_at": "2026-11-14T10:00:00Z", "feedback_due_at": "2027-02-14T10:00:00Z", "acknowledgment_overdue": false, "feedback_overdue": false, "url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef.json", "web_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef", "audit_trail_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports/aBcDef/audit-log.json" } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, invalid, expired or revoked bearer token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **WWW-Authenticate** string Bearer realm=“EthicsPortal” ### 403 Required agreement not accepted or permission denied application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "agreement_required", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Missing or inaccessible organisation or resource, or feature disabled application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` #### Headers **Retry-After** string 60 seconds --- # Create an organisation Source: https://ethicsportal.eu/developers/api/operations/createorganization/ POST /organizations.json ``` const url = 'https://secure.ethicsportal.eu/organizations.json'; const options = { method: 'POST', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"organization":{"name":"Example organisation"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "organization": { "name": "Example organisation" } }' ``` * EthicsPortal application Create an organisation. Requires organizations:create, which only personal tokens can hold, current user terms, and at least one active membership. An assistant connection is the exception: a person with no organisation can create their first one through it. The owner and an administrator membership are assigned from the token user. Creates the default inactive whistleblowing channel. No billing or DPA acceptance is performed: the owner accepts the DPA in a browser, and until then the organisation’s operations answer agreement\_required. Limited to 10 creation attempts per user per minute, across tokens. Send an Idempotency-Key so a retry after a lost response returns the first response instead of creating a second organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** organizations:create ## Parameters ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Request Bodyrequired application/json object **organization** required object **name** required string \>= 1 characters <= 100 characters ### Example ``` { "organization": { "name": "Example organisation" } } ``` ## Responses ### 201 Organisation and owner membership created, with a default channel. application/json object **id** required string **name** required string **membership\_id** required The creating user’s administrator membership, as an obfuscated ID. Use as compliance\_officer\_id to appoint yourself. string **channel** required object **id** required string **active** required boolean **reports\_url** required string format: uri **channel\_url** required string format: uri **members\_url** required string format: uri **webhooks\_url** required string format: uri **audit\_trail\_url** required string format: uri **documents\_url** required string format: uri **billing\_url** required string format: uri #### Example ``` { "id": "gHiJkl", "name": "Example organisation", "membership_id": "mAbCde", "channel": { "id": "AB12CD34", "active": false }, "reports_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/reports.json", "channel_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/portal.json", "members_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/members.json", "webhooks_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks.json", "audit_trail_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/audit_logs.json", "documents_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/documents.json", "billing_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/subscriptions.json" } ``` #### Headers **Location** string format: uri Organisation web page; requires a separate authenticated browser session. ### 400 Invalid JSON body, attributes or types. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing or unusable token. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 Required scope missing, a personal token whose user is not a current administrator, or agreement\_required until browser acceptance. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "forbidden", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource inaccessible, or API access suspended. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Model validation failed. Fields are attribute names only; values and personal data are never reflected. application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "name" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded. Retry-After: 60. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Read channel configuration Source: https://ethicsportal.eu/developers/api/operations/getchannelconfiguration/ GET /organizations/{organization\_id}/whistleblower/portal/configuration.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/portal/configuration.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/portal/configuration.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Read the channel’s company details, settings and compliance officer. Requires channel:write. A personal token also needs its user to be a current administrator; an organisation token needs only the scope. This privileged resource includes company details and the compliance officer membership ID; the ordinary channel metadata endpoint remains unchanged. Organisation integrations and the current DPA must be enabled/accepted. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** channel:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** channel:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Current settings and derived readiness. application/json object **legal\_name** required string | null <= 255 characters **country\_code** required A supported ISO 3166-1 alpha-2 country code. Country-specific retention minimums apply. string | null **registration\_number** required string | null **address** required string | null **employee\_range** required string | null Allowed values: 1-49 50-249 250+ "" **industry** required string | null Allowed values: agriculture\_forestry mining\_quarrying manufacturing energy\_utilities water\_waste\_management construction wholesale\_retail\_trade transportation\_logistics hospitality\_food\_services information\_communication financial\_services\_insurance real\_estate professional\_scientific\_services administrative\_support\_services public\_administration\_defence education healthcare\_social\_work arts\_entertainment\_recreation other "" **website** required string | null **phone\_number** required string | null **compliance\_officer\_id** required An active administrator membership in this organisation, identified by its obfuscated ID. Creation returns your membership\_id. Null removes the appointment and can make the channel inactive. Raw database IDs are rejected. string | null **brand\_color** required string | null **retention\_months** required Must also meet the selected country’s minimum. Defaults to 60 months. A country change and its retention adjustment are validated together. integer Allowed values: 12 24 36 48 60 **social\_dialogue\_consulted** required boolean **voice\_recording\_enabled** required boolean **email\_intake\_enabled** required boolean **id** required string **active** required boolean **verified** required boolean **reporting\_form\_url** required string format: uri **missing\_fields** required Array unique items Allowed values: legal\_name country\_code registration\_number address employee\_range industry compliance\_officer\_id #### Example ``` { "legal_name": "Example company", "country_code": "FR", "registration_number": "EXAMPLE-123", "address": "1 Example Street", "employee_range": "1-49", "industry": "professional_scientific_services", "website": null, "phone_number": null, "compliance_officer_id": "mAbCde", "brand_color": "#123456", "retention_months": 60, "social_dialogue_consulted": false, "voice_recording_enabled": false, "email_intake_enabled": false, "id": "AB12CD34", "active": false, "verified": false, "reporting_form_url": "https://secure.ethicsportal.eu/p/AB12CD34", "missing_fields": [] } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid JSON body, attributes or types. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing or unusable token. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 Required scope missing, a personal token whose user is not a current administrator, or agreement\_required until browser acceptance. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "forbidden", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource inaccessible, or API access suspended. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded. Retry-After: 60. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Configure a whistleblowing channel Source: https://ethicsportal.eu/developers/api/operations/updatechannelconfiguration/ PATCH /organizations/{organization\_id}/whistleblower/portal/configuration.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/portal/configuration.json'; const options = { method: 'PATCH', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"portal":{"legal_name":"Example company","country_code":"FR","registration_number":"EXAMPLE-123","address":"1 Example Street","employee_range":"1-49","industry":"professional_scientific_services","website":null,"phone_number":null,"compliance_officer_id":"mAbCde","brand_color":"#123456","retention_months":60,"social_dialogue_consulted":false,"voice_recording_enabled":false,"email_intake_enabled":false}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/portal/configuration.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "portal": { "legal_name": "Example company", "country_code": "FR", "registration_number": "EXAMPLE-123", "address": "1 Example Street", "employee_range": "1-49", "industry": "professional_scientific_services", "website": null, "phone_number": null, "compliance_officer_id": "mAbCde", "brand_color": "#123456", "retention_months": 60, "social_dialogue_consulted": false, "voice_recording_enabled": false, "email_intake_enabled": false } }' ``` * EthicsPortal application Change the channel’s company details and settings. Requires channel:write. A personal token also needs its user to be a current administrator; an organisation token needs only the scope. Send application/json with a nonempty portal object; only supplied fields change. Unknown fields, query parameters and incorrect JSON types are rejected. Verified company details remain locked. Retention minimums, active administrator appointments and readiness rules are the same as the web application. active, verified, slug, billing and organisation flags cannot be set. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** channel:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** channel:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID. ## Request Bodyrequired application/json object **portal** required object \>= 1 properties **legal\_name** string | null <= 255 characters **country\_code** A supported ISO 3166-1 alpha-2 country code. Country-specific retention minimums apply. string | null **registration\_number** string | null **address** string | null **employee\_range** string | null Allowed values: 1-49 50-249 250+ "" **industry** string | null Allowed values: agriculture\_forestry mining\_quarrying manufacturing energy\_utilities water\_waste\_management construction wholesale\_retail\_trade transportation\_logistics hospitality\_food\_services information\_communication financial\_services\_insurance real\_estate professional\_scientific\_services administrative\_support\_services public\_administration\_defence education healthcare\_social\_work arts\_entertainment\_recreation other "" **website** string | null **phone\_number** string | null **compliance\_officer\_id** An active administrator membership in this organisation, identified by its obfuscated ID. Creation returns your membership\_id. Null removes the appointment and can make the channel inactive. Raw database IDs are rejected. string | null **brand\_color** string | null **retention\_months** Must also meet the selected country’s minimum. Defaults to 60 months. A country change and its retention adjustment are validated together. integer Allowed values: 12 24 36 48 60 **social\_dialogue\_consulted** boolean **voice\_recording\_enabled** boolean **email\_intake\_enabled** boolean ### Example ``` { "portal": { "legal_name": "Example company", "country_code": "FR", "registration_number": "EXAMPLE-123", "address": "1 Example Street", "employee_range": "1-49", "industry": "professional_scientific_services", "website": null, "phone_number": null, "compliance_officer_id": "mAbCde", "brand_color": "#123456", "retention_months": 60, "social_dialogue_consulted": false, "voice_recording_enabled": false, "email_intake_enabled": false } } ``` ## Responses ### 200 Validated settings saved. active is derived, never a promise of activation. application/json object **legal\_name** required string | null <= 255 characters **country\_code** required A supported ISO 3166-1 alpha-2 country code. Country-specific retention minimums apply. string | null **registration\_number** required string | null **address** required string | null **employee\_range** required string | null Allowed values: 1-49 50-249 250+ "" **industry** required string | null Allowed values: agriculture\_forestry mining\_quarrying manufacturing energy\_utilities water\_waste\_management construction wholesale\_retail\_trade transportation\_logistics hospitality\_food\_services information\_communication financial\_services\_insurance real\_estate professional\_scientific\_services administrative\_support\_services public\_administration\_defence education healthcare\_social\_work arts\_entertainment\_recreation other "" **website** required string | null **phone\_number** required string | null **compliance\_officer\_id** required An active administrator membership in this organisation, identified by its obfuscated ID. Creation returns your membership\_id. Null removes the appointment and can make the channel inactive. Raw database IDs are rejected. string | null **brand\_color** required string | null **retention\_months** required Must also meet the selected country’s minimum. Defaults to 60 months. A country change and its retention adjustment are validated together. integer Allowed values: 12 24 36 48 60 **social\_dialogue\_consulted** required boolean **voice\_recording\_enabled** required boolean **email\_intake\_enabled** required boolean **id** required string **active** required boolean **verified** required boolean **reporting\_form\_url** required string format: uri **missing\_fields** required Array unique items Allowed values: legal\_name country\_code registration\_number address employee\_range industry compliance\_officer\_id #### Example ``` { "legal_name": "Example company", "country_code": "FR", "registration_number": "EXAMPLE-123", "address": "1 Example Street", "employee_range": "1-49", "industry": "professional_scientific_services", "website": null, "phone_number": null, "compliance_officer_id": "mAbCde", "brand_color": "#123456", "retention_months": 60, "social_dialogue_consulted": false, "voice_recording_enabled": false, "email_intake_enabled": false, "id": "AB12CD34", "active": false, "verified": false, "reporting_form_url": "https://secure.ethicsportal.eu/p/AB12CD34", "missing_fields": [] } ``` ### 400 Invalid JSON body, attributes or types. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing or unusable token. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 Required scope missing, a personal token whose user is not a current administrator, or agreement\_required until browser acceptance. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "forbidden", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource inaccessible, or API access suspended. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Model validation failed. Fields are attribute names only; values and personal data are never reflected. application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "retention_months" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded. Retry-After: 60. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List webhooks Source: https://ethicsportal.eu/developers/api/operations/listwebhooks/ GET /organizations/{organization\_id}/webhooks.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/webhooks.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application List the organisation’s webhooks. Secrets are never included. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 The organisation’s webhooks (at most ten). application/json object **data** required Array <= 10 items object **id** required string **name** required string <= 100 characters **url** required The destination EthicsPortal sends events to, as in Stripe’s API. Other \*\_url fields are links within this API. string format: uri **enabled** required boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue **failing\_since** required Set when a delivery failed permanently or exhausted its retries; cleared by a later success or a configuration change. string | null format: date-time **created\_at** required string format: date-time **updated\_at** required string format: date-time **deliveries\_url** required string format: uri **next\_cursor** required null #### Example ``` { "data": [ { "id": "pQrStu", "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "enabled": true, "events": [ "report.created", "report.overdue" ], "failing_since": null, "created_at": "2026-11-09T10:00:00Z", "updated_at": "2026-11-09T10:00:00Z", "deliveries_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks/pQrStu/deliveries.json" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Create a webhook Source: https://ethicsportal.eu/developers/api/operations/createwebhook/ POST /organizations/{organization\_id}/webhooks.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks.json'; const options = { method: 'POST', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"webhook":{"name":"Case dashboard","url":"https://receiver.example/ethicsportal","events":["report.created","report.overdue"]}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/webhooks.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "webhook": { "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "events": [ "report.created", "report.overdue" ] } }' ``` * EthicsPortal application Register a public HTTPS destination for the selected events. The response contains the signing secret once. A personal token also needs its user to be a current administrator of the organisation. Each organisation can have up to ten webhooks. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Request Bodyrequired application/json object **webhook** required object **name** required string \>= 1 characters <= 100 characters **url** required Public HTTPS URL on port 443\. Redirects are not followed. string **enabled** boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue ### Example ``` { "webhook": { "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "events": [ "report.created", "report.overdue" ] } } ``` ## Responses ### 201 Webhook created; store the secret now. application/json object **id** required string **name** required string <= 100 characters **url** required The destination EthicsPortal sends events to, as in Stripe’s API. Other \*\_url fields are links within this API. string format: uri **enabled** required boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue **failing\_since** required Set when a delivery failed permanently or exhausted its retries; cleared by a later success or a configuration change. string | null format: date-time **created\_at** required string format: date-time **updated\_at** required string format: date-time **secret** required Signing secret. Returned only when created or rotated; store it immediately. string **deliveries\_url** required string format: uri #### Example ``` { "id": "pQrStu", "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "enabled": true, "events": [ "report.created", "report.overdue" ], "failing_since": null, "created_at": "2026-11-09T10:00:00Z", "updated_at": "2026-11-09T10:00:00Z", "secret": "ep_whsec_example", "deliveries_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks/pQrStu/deliveries.json" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Validation failed; fields are named, values are not echoed application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "url" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Read a webhook Source: https://ethicsportal.eu/developers/api/operations/getwebhook/ GET /organizations/{organization\_id}/webhooks/{webhook\_id}.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Read one webhook without its secret. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 The webhook. application/json object **id** required string **name** required string <= 100 characters **url** required The destination EthicsPortal sends events to, as in Stripe’s API. Other \*\_url fields are links within this API. string format: uri **enabled** required boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue **failing\_since** required Set when a delivery failed permanently or exhausted its retries; cleared by a later success or a configuration change. string | null format: date-time **created\_at** required string format: date-time **updated\_at** required string format: date-time **deliveries\_url** required string format: uri #### Example ``` { "id": "pQrStu", "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "enabled": true, "events": [ "report.created", "report.overdue" ], "failing_since": null, "created_at": "2026-11-09T10:00:00Z", "updated_at": "2026-11-09T10:00:00Z", "deliveries_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks/pQrStu/deliveries.json" } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Delete a webhook Source: https://ethicsportal.eu/developers/api/operations/deletewebhook/ DELETE /organizations/{organization\_id}/webhooks/{webhook\_id}.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example.json'; const options = {method: 'DELETE', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Delete the webhook and its delivery history. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ## Responses ### 204 Deleted. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Update a webhook Source: https://ethicsportal.eu/developers/api/operations/updatewebhook/ PATCH /organizations/{organization\_id}/webhooks/{webhook\_id}.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example.json'; const options = { method: 'PATCH', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"webhook":{"enabled":false}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "webhook": { "enabled": false } }' ``` * EthicsPortal application Change the name, URL, enabled state or events; only supplied fields change. Changing the URL, events or enabled state cancels pending deliveries from the previous configuration. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ## Request Bodyrequired application/json object **webhook** required object \>= 1 properties **name** string \>= 1 characters <= 100 characters **url** Public HTTPS URL on port 443\. Redirects are not followed. string **enabled** boolean **events** Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue ### Example ``` { "webhook": { "enabled": false } } ``` ## Responses ### 200 The updated webhook. application/json object **id** required string **name** required string <= 100 characters **url** required The destination EthicsPortal sends events to, as in Stripe’s API. Other \*\_url fields are links within this API. string format: uri **enabled** required boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue **failing\_since** required Set when a delivery failed permanently or exhausted its retries; cleared by a later success or a configuration change. string | null format: date-time **created\_at** required string format: date-time **updated\_at** required string format: date-time **deliveries\_url** required string format: uri #### Example ``` { "id": "pQrStu", "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "enabled": false, "events": [ "report.created", "report.overdue" ], "failing_since": null, "created_at": "2026-11-09T10:00:00Z", "updated_at": "2026-11-09T10:00:00Z", "deliveries_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks/pQrStu/deliveries.json" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Validation failed; fields are named, values are not echoed application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "events" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Rotate a webhook's signing secret Source: https://ethicsportal.eu/developers/api/operations/rotatewebhooksecret/ POST /organizations/{organization\_id}/webhooks/{webhook\_id}/rotate.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example/rotate.json'; const options = {method: 'POST', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example/rotate.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Replace the signing secret immediately; there is no overlap period. Pending deliveries from the previous secret are cancelled. The response contains the new secret once. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Responses ### 200 The webhook with its new secret. application/json object **id** required string **name** required string <= 100 characters **url** required The destination EthicsPortal sends events to, as in Stripe’s API. Other \*\_url fields are links within this API. string format: uri **enabled** required boolean **events** required Array \>= 1 items unique items Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue **failing\_since** required Set when a delivery failed permanently or exhausted its retries; cleared by a later success or a configuration change. string | null format: date-time **created\_at** required string format: date-time **updated\_at** required string format: date-time **secret** required Signing secret. Returned only when created or rotated; store it immediately. string **deliveries\_url** required string format: uri #### Example ``` { "id": "pQrStu", "name": "Case dashboard", "url": "https://receiver.example/ethicsportal", "enabled": true, "events": [ "report.created", "report.overdue" ], "failing_since": null, "created_at": "2026-11-09T10:00:00Z", "updated_at": "2026-11-09T10:00:00Z", "secret": "ep_whsec_example", "deliveries_url": "https://secure.ethicsportal.eu/organizations/gHiJkl/webhooks/pQrStu/deliveries.json" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Send a test event Source: https://ethicsportal.eu/developers/api/operations/testwebhook/ POST /organizations/{organization\_id}/webhooks/{webhook\_id}/test.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example/test.json'; const options = {method: 'POST', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example/test.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Queue a webhook.test event containing only the organisation ID. A queued delivery is not proof of receipt; check its status. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Responses ### 202 Test delivery queued. application/json object **id** required string **event\_id** required Matches the payload id and the X-EthicsPortal-Event-Id header. string **event\_type** required string Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue webhook.test **status** required string Allowed values: pending delivering delivered failed cancelled **attempts** required integer **response\_code** required integer | null **error\_code** required string | null Allowed values: connection\_failed unsafe\_destination **created\_at** required string format: date-time **delivered\_at** required string | null format: date-time **next\_attempt\_at** required Earliest time of the next scheduled attempt for a pending delivery of the current configuration; queue delays can postpone it. string | null format: date-time #### Example ``` { "id": "vWxYza", "event_id": "bCdEfg", "event_type": "webhook.test", "status": "pending", "attempts": 0, "response_code": null, "error_code": null, "created_at": "2026-11-09T10:05:00Z", "delivered_at": null, "next_attempt_at": null } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List webhook deliveries Source: https://ethicsportal.eu/developers/api/operations/listwebhookdeliveries/ GET /organizations/{organization\_id}/webhooks/{webhook\_id}/deliveries.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example/deliveries.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/webhooks/example/deliveries.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Deliveries retained for 30 days, oldest first. Receiver response bodies are never stored. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Deliveries. application/json object **data** required Array <= 100 items object **id** required string **event\_id** required Matches the payload id and the X-EthicsPortal-Event-Id header. string **event\_type** required string Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue webhook.test **status** required string Allowed values: pending delivering delivered failed cancelled **attempts** required integer **response\_code** required integer | null **error\_code** required string | null Allowed values: connection\_failed unsafe\_destination **created\_at** required string format: date-time **delivered\_at** required string | null format: date-time **next\_attempt\_at** required Earliest time of the next scheduled attempt for a pending delivery of the current configuration; queue delays can postpone it. string | null format: date-time **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "vWxYza", "event_id": "bCdEfg", "event_type": "webhook.test", "status": "pending", "attempts": 0, "response_code": null, "error_code": null, "created_at": "2026-11-09T10:05:00Z", "delivered_at": null, "next_attempt_at": null } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Retry a failed delivery Source: https://ethicsportal.eu/developers/api/operations/retrywebhookdelivery/ POST /organizations/{organization\_id}/webhooks/{webhook\_id}/deliveries/{delivery\_id}/retry.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/webhooks/example/deliveries/example/retry.json'; const options = {method: 'POST', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/webhooks/example/deliveries/example/retry.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Retry a failed delivery with the webhook’s current URL and secret, keeping its event ID. Only failed deliveries can be retried. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** webhooks:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** webhooks:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **webhook\_id** required string **delivery\_id** required string ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Responses ### 202 Retry queued. application/json object **id** required string **event\_id** required Matches the payload id and the X-EthicsPortal-Event-Id header. string **event\_type** required string Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed report.due\_soon report.overdue webhook.test **status** required string Allowed values: pending delivering delivered failed cancelled **attempts** required integer **response\_code** required integer | null **error\_code** required string | null Allowed values: connection\_failed unsafe\_destination **created\_at** required string format: date-time **delivered\_at** required string | null format: date-time **next\_attempt\_at** required Earliest time of the next scheduled attempt for a pending delivery of the current configuration; queue delays can postpone it. string | null format: date-time #### Example ``` { "id": "vWxYza", "event_id": "bCdEfg", "event_type": "report.created", "status": "pending", "attempts": 0, "response_code": null, "error_code": null, "created_at": "2026-11-09T10:05:00Z", "delivered_at": null, "next_attempt_at": null } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The delivery is not failed, the Idempotency-Key was used for a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_retryable", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List the audit trail Source: https://ethicsportal.eu/developers/api/operations/listaudittrail/ GET /organizations/{organization\_id}/whistleblower/audit\_logs.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/audit_logs.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/whistleblower/audit_logs.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application The organisation’s audit trail: who did what and when. Omits anything a reporter did and entries about messages, notes and retaliation, consistent with the data boundary. Entries carry no change details, IP addresses or email addresses. Oldest first. A personal token needs its user to be a current administrator or viewer. Each request is itself recorded in the trail. Entries appear once they are a minute old, so a collector resuming with created\_after cannot skip one that committed late. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** audit\_log:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** audit\_log:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. **created\_after** string format: date-time Exclusive lower bound; resume a log collector from the last entry it stored. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Audit trail entries. application/json object **data** required Array <= 100 items object **id** required string **action** required Stable action name, such as whistleblower.report.viewed or invitation.created. string **actor** required object **kind** required string Allowed values: member system scim sso api mcp admin **name** required string | null **membership\_id** required string | null **subject** required object **type** required string Allowed values: report channel organization membership invitation webhook organization\_token personal\_token data\_export other **id** required string **created\_at** required string format: date-time **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "hIjKlm", "action": "whistleblower.report.viewed", "actor": { "kind": "member", "name": "Anna Novak", "membership_id": "nOpQrs" }, "subject": { "type": "report", "id": "aBcDef" }, "created_at": "2026-11-09T10:00:00Z" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List a report's audit trail Source: https://ethicsportal.eu/developers/api/operations/listreportaudittrail/ GET /organizations/{organization\_id}/whistleblower/reports/{id}/audit-log.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/reports/example/audit-log.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/whistleblower/reports/example/audit-log.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application One report’s audit trail. Omits anything a reporter did and entries about messages, notes and retaliation, consistent with the data boundary. Entries carry no change details, IP addresses or email addresses. Oldest first. A personal token needs access to the report. Entries appear once they are a minute old, so a collector resuming with created\_after cannot skip one that committed late. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** audit\_log:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** audit\_log:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **id** required string Obfuscated report ID; not the reporter access code. ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. **created\_after** string format: date-time Exclusive lower bound; resume a log collector from the last entry it stored. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Audit trail entries for the report. application/json object **data** required Array <= 100 items object **id** required string **action** required Stable action name, such as whistleblower.report.viewed or invitation.created. string **actor** required object **kind** required string Allowed values: member system scim sso api mcp admin **name** required string | null **membership\_id** required string | null **subject** required object **type** required string Allowed values: report channel organization membership invitation webhook organization\_token personal\_token data\_export other **id** required string **created\_at** required string format: date-time **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "hIjKlm", "action": "whistleblower.report.viewed", "actor": { "kind": "member", "name": "Anna Novak", "membership_id": "nOpQrs" }, "subject": { "type": "report", "id": "aBcDef" }, "created_at": "2026-11-09T10:00:00Z" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List members Source: https://ethicsportal.eu/developers/api/operations/listmembers/ GET /organizations/{organization\_id}/members.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/members.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/members.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application The organisation’s members. Every member can list active members; administrators and organisation tokens also see deactivated ones. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** members:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** members:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Members. application/json object **data** required Array <= 100 items object **id** required Membership ID; use as compliance\_officer\_id when the member is an administrator. string **name** required string | null **email** required Null for an erased account. string | null **role** required string Allowed values: admin member viewer **owner** required boolean **active** required boolean **created\_at** required string format: date-time **deactivated\_at** required string | null format: date-time **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "nOpQrs", "name": "Anna Novak", "email": "anna.novak@example.com", "role": "admin", "owner": false, "active": true, "created_at": "2026-10-01T09:00:00Z", "deactivated_at": null } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List pending invitations Source: https://ethicsportal.eu/developers/api/operations/listinvitations/ GET /organizations/{organization\_id}/invitations.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/invitations.json?limit=50'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url 'https://secure.ethicsportal.eu/organizations/example/invitations.json?limit=50' \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Invitations not yet accepted, declined or expired. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** members:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** members:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Query Parameters **limit** integer default: 50 \>= 1 <= 100 **cursor** string Follow the Link response header unchanged. Signed cursor expires after one hour. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Pending invitations. application/json object **data** required Array <= 100 items object **id** required string **email** required string **role** required string Allowed values: admin member viewer **expires\_at** required string format: date-time **last\_sent\_at** required string | null format: date-time **created\_at** required string format: date-time **next\_cursor** required Pass as the cursor parameter for the next page; null on the last page. The Link header carries the same next page. string | null #### Example ``` { "data": [ { "id": "tUvWxy", "email": "new.handler@example.com", "role": "member", "expires_at": "2026-11-23T10:00:00Z", "last_sent_at": "2026-11-09T10:00:00Z", "created_at": "2026-11-09T10:00:00Z" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Invite a member Source: https://ethicsportal.eu/developers/api/operations/createinvitation/ POST /organizations/{organization\_id}/invitations.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/invitations.json'; const options = { method: 'POST', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"invitation":{"email":"new.handler@example.com","role":"member"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/invitations.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "invitation": { "email": "new.handler@example.com", "role": "member" } }' ``` * EthicsPortal application Email an invitation that is valid for 14 days. The invitee accepts it in EthicsPortal; the API never returns the acceptance link. Up to 30 invitations per organisation per hour. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** members:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** members:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Request Bodyrequired application/json object **invitation** required object **email** required string format: email **role** required string Allowed values: admin member viewer ### Example ``` { "invitation": { "email": "new.handler@example.com", "role": "member" } } ``` ## Responses ### 201 Invitation sent. application/json object **id** required string **email** required string **role** required string Allowed values: admin member viewer **expires\_at** required string format: date-time **last\_sent\_at** required string | null format: date-time **created\_at** required string format: date-time #### Example ``` { "id": "tUvWxy", "email": "new.handler@example.com", "role": "member", "expires_at": "2026-11-23T10:00:00Z", "last_sent_at": "2026-11-09T10:00:00Z", "created_at": "2026-11-09T10:00:00Z" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Validation failed; fields are named, values are not echoed application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "email" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Revoke an invitation Source: https://ethicsportal.eu/developers/api/operations/revokeinvitation/ DELETE /organizations/{organization\_id}/invitations/{invitation\_id}.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/invitations/example.json'; const options = {method: 'DELETE', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request DELETE \ --url https://secure.ethicsportal.eu/organizations/example/invitations/example.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Revoke a pending invitation. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** members:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** members:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **invitation\_id** required string ## Responses ### 204 Revoked. ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Resend an invitation Source: https://ethicsportal.eu/developers/api/operations/resendinvitation/ POST /organizations/{organization\_id}/invitations/{invitation\_id}/resend.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/invitations/example/resend.json'; const options = {method: 'POST', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/invitations/example/resend.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Send the invitation email again and restart its 14-day validity. Allowed once every two minutes per invitation; sooner returns 429\. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** members:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** members:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. **invitation\_id** required string ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Responses ### 200 Invitation resent. application/json object **id** required string **email** required string **role** required string Allowed values: admin member viewer **expires\_at** required string format: date-time **last\_sent\_at** required string | null format: date-time **created\_at** required string format: date-time #### Example ``` { "id": "tUvWxy", "email": "new.handler@example.com", "role": "member", "expires_at": "2026-11-23T10:00:00Z", "last_sent_at": "2026-11-09T10:00:00Z", "created_at": "2026-11-09T10:00:00Z" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Rename the organisation Source: https://ethicsportal.eu/developers/api/operations/updateorganization/ PATCH /organizations/{organization\_id}/profile.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/profile.json'; const options = { method: 'PATCH', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"organization":{"name":"Example organisation"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://secure.ethicsportal.eu/organizations/example/profile.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "organization": { "name": "Example organisation" } }' ``` * EthicsPortal application Change the organisation’s name. A personal token also needs its user to be a current administrator of the organisation. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** organizations:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** organizations:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Request Bodyrequired application/json object **organization** required object **name** required string \>= 1 characters ### Example ``` { "organization": { "name": "Example organisation" } } ``` ## Responses ### 200 The renamed organisation. application/json object **id** required string **name** required string #### Example ``` { "id": "gHiJkl", "name": "Example organisation" } ``` ### 400 Invalid query, body or cursor application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation or resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Validation failed; fields are named, values are not echoed application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "name" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List my invitations Source: https://ethicsportal.eu/developers/api/operations/listmyinvitations/ GET /me/invitations.json ``` const url = 'https://secure.ethicsportal.eu/me/invitations.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/me/invitations.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Pending invitations addressed to the token’s user. The acceptance link is never returned. Personal tokens only. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** invitations:respond ## Parameters ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Pending invitations. application/json object **data** required Array object **id** required string **organization** required object **id** required string **name** required string **role** required string Allowed values: admin member viewer **expires\_at** required string format: date-time **created\_at** required string format: date-time **accept\_url** required string format: uri **decline\_url** required string format: uri **next\_cursor** required null #### Example ``` { "data": [ { "id": "tUvWxy", "organization": { "id": "gHiJkl", "name": "Example organisation" }, "role": "member", "expires_at": "2026-11-23T10:00:00Z", "created_at": "2026-11-09T10:00:00Z", "accept_url": "https://secure.ethicsportal.eu/me/invitations/tUvWxy/accept.json", "decline_url": "https://secure.ethicsportal.eu/me/invitations/tUvWxy/decline.json" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Accept an invitation Source: https://ethicsportal.eu/developers/api/operations/acceptinvitation/ PATCH /me/invitations/{invitation\_id}/accept.json ``` const url = 'https://secure.ethicsportal.eu/me/invitations/example/accept.json'; const options = {method: 'PATCH', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://secure.ethicsportal.eu/me/invitations/example/accept.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Join the organisation with the invited role. The organisation’s own agreements still apply before its data can be read. Personal tokens only. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** invitations:respond ## Parameters ### Path Parameters **invitation\_id** required string ## Responses ### 200 The new membership. application/json object **id** required Membership ID; use as compliance\_officer\_id when the member is an administrator. string **name** required string | null **email** required Null for an erased account. string | null **role** required string Allowed values: admin member viewer **owner** required boolean **active** required boolean **created\_at** required string format: date-time **deactivated\_at** required string | null format: date-time #### Example ``` { "id": "nOpQrs", "name": "Anna Novak", "email": "anna.novak@example.com", "role": "member", "owner": false, "active": true, "created_at": "2026-11-09T10:05:00Z", "deactivated_at": null } ``` ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The invitation is no longer valid: expired, already answered, or you are already an active member application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invitation_unavailable", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Decline an invitation Source: https://ethicsportal.eu/developers/api/operations/declineinvitation/ PATCH /me/invitations/{invitation\_id}/decline.json ``` const url = 'https://secure.ethicsportal.eu/me/invitations/example/decline.json'; const options = {method: 'PATCH', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request PATCH \ --url https://secure.ethicsportal.eu/me/invitations/example/decline.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Decline a pending invitation. Personal tokens only. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** invitations:respond ## Parameters ### Path Parameters **invitation\_id** required string ## Responses ### 204 Declined. ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The invitation is no longer valid: expired, already answered, or you are already an active member application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invitation_unavailable", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # List documents Source: https://ethicsportal.eu/developers/api/operations/listdocuments/ GET /organizations/{organization\_id}/whistleblower/documents.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application The documents the token may download, with their download URLs. A personal token sees compliance evidence only if its user is an administrator or viewer. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Documents. application/json object **data** required Array <= 6 items object **type** required string Allowed values: compliance\_report compliance\_certificate whistleblower\_policy privacy\_notice poster case\_handler\_manual **content\_type** required string Allowed values: application/pdf application/vnd.openxmlformats-officedocument.wordprocessingml.document **available** required False while the whistleblowing channel is inactive; downloads then return 409 channel\_inactive. boolean **url** required string format: uri **next\_cursor** required null #### Example ``` { "data": [ { "type": "poster", "content_type": "application/pdf", "available": true, "url": "https://secure.ethicsportal.eu/organizations/gHiJkl/whistleblower/documents/poster.pdf" } ], "next_cursor": null } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the compliance report Source: https://ethicsportal.eu/developers/api/operations/downloadcompliancereport/ GET /organizations/{organization\_id}/whistleblower/documents/compliance\_report.pdf ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/compliance_report.pdf'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/compliance_report.pdf \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Evidence of the channel’s configuration and handling, for auditors. Administrators and viewers only. Generated on request from the channel’s current settings. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The compliance report as PDF. application/pdf string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the compliance certificate Source: https://ethicsportal.eu/developers/api/operations/downloadcompliancecertificate/ GET /organizations/{organization\_id}/whistleblower/documents/compliance\_certificate.pdf ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/compliance_certificate.pdf'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/compliance_certificate.pdf \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Certificate of an active whistleblowing channel. Administrators and viewers only. Generated on request from the channel’s current settings. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The compliance certificate as PDF. application/pdf string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the whistleblowing policy Source: https://ethicsportal.eu/developers/api/operations/downloadwhistleblowerpolicy/ GET /organizations/{organization\_id}/whistleblower/documents/whistleblower\_policy.docx ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/whistleblower_policy.docx'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/whistleblower_policy.docx \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Editable policy for staff, filled with the channel’s details. Generated on request from the channel’s current settings. Any member’s personal token can download it. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The whistleblowing policy as DOCX. application/vnd.openxmlformats-officedocument.wordprocessingml.document string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the privacy notice Source: https://ethicsportal.eu/developers/api/operations/downloadprivacynotice/ GET /organizations/{organization\_id}/whistleblower/documents/privacy\_notice.docx ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/privacy_notice.docx'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/privacy_notice.docx \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Editable privacy notice for the whistleblowing channel. Generated on request from the channel’s current settings. Any member’s personal token can download it. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The privacy notice as DOCX. application/vnd.openxmlformats-officedocument.wordprocessingml.document string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the poster Source: https://ethicsportal.eu/developers/api/operations/downloadposter/ GET /organizations/{organization\_id}/whistleblower/documents/poster.pdf ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/poster.pdf'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/poster.pdf \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Poster with the online reporting form address and QR code. Generated on request from the channel’s current settings. Any member’s personal token can download it. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The poster as PDF. application/pdf string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Download the case handler manual Source: https://ethicsportal.eu/developers/api/operations/downloadcasehandlermanual/ GET /organizations/{organization\_id}/whistleblower/documents/case\_handler\_manual.pdf ``` const url = 'https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/case_handler_manual.pdf'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/whistleblower/documents/case_handler_manual.pdf \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Manual for the people who handle reports. Generated on request from the channel’s current settings. Any member’s personal token can download it. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** documents:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** documents:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ## Responses ### 200 The case handler manual as PDF. application/pdf string format: binary ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The whistleblowing channel is not active yet, so its documents cannot be generated application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "channel_inactive", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Read billing status Source: https://ethicsportal.eu/developers/api/operations/getbillingstatus/ GET /organizations/{organization\_id}/subscriptions.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/subscriptions.json'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request GET \ --url https://secure.ethicsportal.eu/organizations/example/subscriptions.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Access and subscription status, read from EthicsPortal’s synced billing records. Subscribing, changing plan and cancelling stay in the web application. A personal token also needs its user to be a current administrator. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** billing:read * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** billing:read ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **If-None-Match** string The ETag of a response you already hold. If nothing changed, the response is 304 Not Modified with no body. It still counts toward rate limits and, where reads are audited, the audit trail. ## Responses ### 200 Billing status. application/json object **access** required Whether the organisation currently has paid or granted access. The whistleblowing channel can be active only when this is true. boolean **complimentary** required Access granted by EthicsPortal without a subscription. boolean **subscription** required object **status** required Stripe subscription status, such as active, trialing, past\_due or canceled. string **interval** required string | null Allowed values: month year **current\_period\_end** required string | null format: date-time **ends\_at** required Set when the subscription has been cancelled and access ends at this time. string | null format: date-time **trial\_ends\_at** required string | null format: date-time #### Example ``` { "access": true, "complimentary": false, "subscription": { "status": "active", "interval": "year", "current_period_end": "2027-11-09T10:00:00Z", "ends_at": null, "trial_ends_at": null } } ``` #### Headers **ETag** string Weak validator of this response body. Send it back as If-None-Match. ### 304 Not modified since the ETag sent in If-None-Match. No body. ### 400 Invalid query or parameters application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource missing or inaccessible, or API access suspended application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Create a subscription checkout link Source: https://ethicsportal.eu/developers/api/operations/createcheckoutlink/ POST /organizations/{organization\_id}/subscriptions/checkout.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/subscriptions/checkout.json'; const options = { method: 'POST', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"subscription":{"interval":"year"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/subscriptions/checkout.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "subscription": { "interval": "year" } }' ``` * EthicsPortal application Return a Stripe Checkout link for the monthly or yearly plan. A person opens it and pays; Stripe handles strong customer authentication, tax and VAT ID collection. Access starts when Stripe confirms the payment, whether or not the payer is signed in to EthicsPortal. Send an Idempotency-Key so a retry returns the same link. A personal token also needs its user to be a current administrator. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** billing:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** billing:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Request Bodyrequired application/json object **subscription** required object **interval** required string Allowed values: month year ### Example ``` { "subscription": { "interval": "year" } } ``` ## Responses ### 201 Checkout link created. application/json object **url** required Stripe-hosted page. Give it to the person who pays or manages billing; it works for whoever holds it until it expires. string format: uri **expires\_at** required When the link stops working. Null for billing portal links, which expire shortly after creation; create one just before use. string | null format: date-time #### Example ``` { "url": "https://checkout.stripe.com/c/pay/cs_live_example", "expires_at": "2026-11-10T10:00:00Z" } ``` ### 400 Invalid body application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation missing or inaccessible, API access suspended, or billing not configured application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The organisation already has a subscription (use the billing portal link), or an Idempotency-Key conflict (This Idempotency-Key was already used with a different request, or its first request is still running) application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "already_subscribed", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Unknown interval application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "interval" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Create a billing portal link Source: https://ethicsportal.eu/developers/api/operations/createbillingportallink/ POST /organizations/{organization\_id}/subscriptions/billing\_portal.json ``` const url = 'https://secure.ethicsportal.eu/organizations/example/subscriptions/billing_portal.json'; const options = {method: 'POST', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/organizations/example/subscriptions/billing_portal.json \ --header 'Authorization: Bearer ' ``` * EthicsPortal application Return a Stripe customer portal link where a person changes the plan, updates the payment method, downloads invoices or cancels. The link manages the subscription for whoever holds it until it expires, so treat it like a credential; each link is recorded in the audit trail. A personal token also needs its user to be a current administrator. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** billing:write * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** billing:write ## Parameters ### Path Parameters **organization\_id** required string Obfuscated organisation ID returned by identity. ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Responses ### 201 Billing portal link created. application/json object **url** required Stripe-hosted page. Give it to the person who pays or manages billing; it works for whoever holds it until it expires. string format: uri **expires\_at** required When the link stops working. Null for billing portal links, which expire shortly after creation; create one just before use. string | null format: date-time #### Example ``` { "url": "https://billing.stripe.com/p/session/live_example", "expires_at": null } ``` ### 400 Invalid body application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing, unknown, expired or revoked token application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 The token lacks the required scope (WWW-Authenticate names it), the user’s role does not allow the action, or an agreement must be accepted in the web application application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "insufficient_scope", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Organisation missing or inaccessible, API access suspended, or billing not configured application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 The organisation has no Stripe customer yet (create a checkout link first), or an Idempotency-Key conflict (This Idempotency-Key was already used with a different request, or its first request is still running) application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "no_subscription", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limited; retry after the Retry-After interval application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Send product feedback Source: https://ethicsportal.eu/developers/api/operations/submitfeedback/ POST /me/feedback.json ``` const url = 'https://secure.ethicsportal.eu/me/feedback.json'; const options = { method: 'POST', headers: {Authorization: 'Bearer ', 'Content-Type': 'application/json'}, body: '{"feedback":{"kind":"bug","message":"The yearly report export times out for large organisations."}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ``` curl --request POST \ --url https://secure.ethicsportal.eu/me/feedback.json \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "feedback": { "kind": "bug", "message": "The yearly report export times out for large organisations." } }' ``` * EthicsPortal application File a bug report, feature request or other feedback to EthicsPortal, as the in-app feedback form does. Any valid token may send it, without a scope; every error response links here as `error.feedback_url`. Feedback is attributed to the token’s user, or to the organisation token itself. Limited to 10 submissions per user or organisation per minute. Do not include report content or personal data about reporters. ## Authorizations * **[personalToken](https://ethicsportal.eu/developers/api/#personaltoken)** * **[organizationToken](https://ethicsportal.eu/developers/api/#organizationtoken)** ## Parameters ### Header Parameters **Idempotency-Key** string \>= 1 characters <= 255 characters /^\[\\x21-\\x7e\]+$/ Recommended on every POST. Retrying with the same key and the same request within 24 hours returns the first response (with Idempotent-Replayed: true) instead of acting twice. Only successful responses and validation errors are stored; any other outcome releases the key. Reusing a key with a different request returns 409 idempotency\_key\_reused; a retry while the first request is still running returns 409 idempotency\_key\_in\_progress. Keys are scoped to the token. ## Request Bodyrequired application/json object **feedback** required object **kind** string default: other Allowed values: bug feature other **message** required string \>= 1 characters ### Example ``` { "feedback": { "kind": "bug", "message": "The yearly report export times out for large organisations." } } ``` ## Responses ### 201 Feedback filed for the EthicsPortal team. application/json object **kind** required string Allowed values: bug feature other **message** required string **status** required string Allowed values: open in\_review resolved **created\_at** required string format: date-time #### Example ``` { "kind": "bug", "message": "The yearly report export times out for large organisations.", "status": "open", "created_at": "2026-11-09T10:05:00Z" } ``` ### 400 Invalid JSON body, attributes or types. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_request", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 401 Missing or unusable token. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "invalid_token", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 403 Required scope missing, a personal token whose user is not a current administrator, or agreement\_required until browser acceptance. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "forbidden", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 404 Resource inaccessible, or API access suspended. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "not_found", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 409 This Idempotency-Key was already used with a different request, or its first request is still running application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "idempotency_key_reused", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 422 Model validation failed. Fields are attribute names only; values and personal data are never reflected. application/json object **error** required object **code** required Allowed value: validation\_failed **fields** required Array \>= 1 items unique items **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "validation_failed", "fields": [ "message" ], "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` ### 429 Rate limit exceeded. Retry-After: 60. application/json object **error** required object **code** required string Allowed values: invalid\_token forbidden insufficient\_scope not\_found invalid\_request agreement\_required rate\_limited idempotency\_key\_reused idempotency\_key\_in\_progress not\_retryable channel\_inactive invitation\_unavailable already\_subscribed no\_subscription **feedback\_url** required Where to tell EthicsPortal what went wrong: POST product feedback here with the same token. string format: uri #### Example ``` { "error": { "code": "rate_limited", "feedback_url": "https://secure.ethicsportal.eu/me/feedback.json" } } ``` --- # Receive a webhook event Source: https://ethicsportal.eu/developers/api/webhooks/receivereportevent/ POST EthicsPortal sends a signed POST request to your HTTPS endpoint when a report is created or changes, or when a deadline is close or has passed. The destination must be public. Verify HMAC-SHA256(timestamp + “.” + raw request body), timestamp freshness and event ID. At-least-once delivery; no ordering guarantee. ## Parameters ### Header Parameters **X-EthicsPortal-Event-Id** required string **X-EthicsPortal-Signature** required string `t=,v1=` ## Request Bodyrequired application/json One of: * [Report lifecycle event](https://ethicsportal.eu/developers/api/webhooks/receivereportevent/#tab-panel-0-0) * [Deadline event](https://ethicsportal.eu/developers/api/webhooks/receivereportevent/#tab-panel-0-1) * [Test event](https://ethicsportal.eu/developers/api/webhooks/receivereportevent/#tab-panel-0-2) _Report lifecycle event_object **id** required string **type** required string Allowed values: report.created report.acknowledged report.feedback\_given report.closed report.reopened report.assignment\_changed **created\_at** required string format: date-time **data** required object **id** required string **organization\_id** required string **status** required string Allowed values: received acknowledged feedback\_given closed **assigned** required boolean **created\_at** required string format: date-time **acknowledged\_at** required string | null format: date-time **feedback\_given\_at** required string | null format: date-time **closed\_at** required string | null format: date-time **acknowledgment\_due\_at** required string format: date-time **feedback\_due\_at** required string format: date-time **acknowledgment\_overdue** required boolean **feedback\_overdue** required boolean _Deadline event_object **id** required string **type** required string Allowed values: report.due\_soon report.overdue **created\_at** required string format: date-time **data** required object **report\_id** required string **organization\_id** required string **deadline\_type** required string Allowed values: acknowledgment feedback **deadline\_at** required string format: date-time _Test event_object **id** required string **type** required string Allowed values: webhook.test **created\_at** required string format: date-time **data** required object **organization\_id** required string ## Responses ### 2XX Receipt accepted. Process asynchronously and deduplicate by event ID. ### default Non-2xx responses are retried; redirects are never followed. --- # Fetch every report Source: https://ethicsportal.eu/developers/examples/pagination/ This Python 3 example uses the standard library. Supply `ETHICSPORTAL_TOKEN` and `ETHICSPORTAL_ORGANIZATION_ID` through your environment, then run the [downloadable example](https://ethicsportal.eu/developers/examples/paginate.py). ``` python3 paginate.py ``` It prints one report metadata object per line. It passes each page’s `next_cursor` back as the `cursor` parameter, so it only ever requests the report collection on the EthicsPortal origin, and it refuses redirects. paginate.py ``` """Fetch authorised report metadata using Python 3's standard library.""" import json import os import urllib.error import urllib.parse import urllib.request ORIGIN = "https://secure.ethicsportal.eu" class NoRedirects(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None def reports(token, organization_id, opener=None): base = f"{ORIGIN}/organizations/{urllib.parse.quote(organization_id, safe='')}/whistleblower/reports.json" opener = opener or urllib.request.build_opener(NoRedirects()) cursor, seen = None, set() while True: query = {"limit": "100", **({"cursor": cursor} if cursor else {})} request = urllib.request.Request(f"{base}?{urllib.parse.urlencode(query)}", headers={ "Authorization": f"Bearer {token}", "Accept": "application/json" }) with opener.open(request, timeout=20) as response: page = json.load(response) if not isinstance(page, dict) or not isinstance(page.get("data"), list): raise ValueError("Expected a report list") yield from page["data"] cursor = page.get("next_cursor") if not cursor: return if cursor in seen: raise ValueError("Repeated pagination cursor; restart reconciliation") seen.add(cursor) if __name__ == "__main__": try: for report in reports(os.environ["ETHICSPORTAL_TOKEN"], os.environ["ETHICSPORTAL_ORGANIZATION_ID"]): print(json.dumps(report)) except urllib.error.HTTPError as error: retry = error.headers.get("Retry-After", "unspecified") if error.code == 429 else None raise SystemExit(f"API returned HTTP {error.code}" + (f"; retry after {retry} seconds" if retry else "")) from None ``` A `429` stops the example and reports the wait interval. In a scheduled integration, wait and retry with bounded backoff. Start a fresh traversal after an expired cursor and reconcile by report ID; permissions can change between pages. The output contains case metadata, so handle it according to your organisation’s access and retention rules. --- # Verify a webhook Source: https://ethicsportal.eu/developers/examples/verify-webhook/ Use this helper in a Node.js receiver. [Download verify-webhook.mjs](https://ethicsportal.eu/developers/examples/verify-webhook.mjs). It uses only the built-in cryptography module. verify-webhook.mjs ``` import { createHmac, timingSafeEqual } from 'node:crypto' export function verifyWebhook({ rawBody, signature, eventId, secret, now = Date.now() }) { if (!Buffer.isBuffer(rawBody) || rawBody.length > 256 * 1024) throw new Error('Invalid webhook body') if (typeof secret !== 'string' || !secret) throw new Error('Missing signing secret') if (typeof signature !== 'string' || typeof eventId !== 'string' || !eventId) throw new Error('Missing webhook headers') const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature) if (!match) throw new Error('Invalid signature header') const timestamp = Number(match[1]) if (!Number.isSafeInteger(timestamp) || !Number.isFinite(now) || Math.abs(now / 1000 - timestamp) > 300) { throw new Error('Webhook timestamp outside tolerance') } const expected = createHmac('sha256', secret).update(`${match[1]}.`).update(rawBody).digest() if (!timingSafeEqual(expected, Buffer.from(match[2], 'hex'))) throw new Error('Invalid webhook signature') const event = JSON.parse(rawBody.toString('utf8')) if (!event || typeof event !== 'object' || event.id !== eventId) throw new Error('Event ID mismatch') return event } ``` ## Connect it to your HTTP handler Read the request body as a `Buffer`, with a size limit enforced **while reading**. Do not let JSON middleware transform it before verification. Pass the raw body, `X-EthicsPortal-Signature`, `X-EthicsPortal-Event-Id` and your secret to `verifyWebhook`. ``` const event = verifyWebhook({ rawBody, signature: request.headers['x-ethicsportal-signature'], eventId: request.headers['x-ethicsportal-event-id'], secret: process.env.ETHICSPORTAL_WEBHOOK_SECRET }) ``` Catch verification failures and return a non-2xx response. Do not log the secret or full body. A valid signature authenticates a delivery; it does not make the delivery unique. ## Persist before acknowledging In your application’s database, insert the verified event into a durable inbox with a unique event ID. On a duplicate, return `2xx` without inserting or applying it again. Return `2xx` for a new event only after the inbox transaction commits. If storage is unavailable, return a failure status. A worker can then process the inbox with idempotent business operations. This helper deliberately handles verification only; the durable inbox belongs in your application. [Delivery and retry semantics](https://ethicsportal.eu/developers/webhooks/). --- # Public developer resources Source: https://ethicsportal.eu/developers/public-resources/ These resources describe EthicsPortal and its published documentation. They do not grant access to customer organisations or reports. | Resource | Address | | ------------------------------------------- | ------------------------------------------------------- | | OpenAPI 3.1 | [Schema](https://ethicsportal.eu/openapi.json) | | API catalog (RFC 9727) | [api-catalog](https://ethicsportal.eu/.well-known/api-catalog) | | Developer documentation index for AI agents | [developers/llms.txt](https://ethicsportal.eu/developers/llms.txt) | | All developer documentation in one file | [developers/llms-full.txt](https://ethicsportal.eu/developers/llms-full.txt) | | Agent guide | [agents.md](https://ethicsportal.eu/agents.md) | | Site overview | [llms.txt](https://ethicsportal.eu/llms.txt) | | Full public content | [llms-full.txt](https://ethicsportal.eu/llms-full.txt) | | Agent card | [A2A card](https://ethicsportal.eu/.well-known/agent-card.json) | | Agent skills | [Skills manifest](https://ethicsportal.eu/.well-known/agent-skills/index.json) | | Sitemap | [sitemap.xml](https://ethicsportal.eu/sitemap.xml) | | Crawl policy | [robots.txt](https://ethicsportal.eu/robots.txt) | | Vulnerability disclosure | [security.txt](https://ethicsportal.eu/.well-known/security.txt) | | Vendor questionnaire | [CAIQ CSV](https://ethicsportal.eu/caiq-ethicsportal.csv) | | Blog feed | [RSS](https://ethicsportal.eu/blog/index.xml) | ## Markdown content negotiation Eligible public pages have a Markdown representation. Append `.md` to a page URL or request `Accept: text/markdown`: ``` curl --fail \ -H 'Accept: text/markdown' \ 'https://ethicsportal.eu/developers/quickstart/' ``` Representation selection honours quality values. `Vary: Accept` distinguishes HTML and Markdown responses. With no preference, the response is HTML. Unsupported preferences receive `406 Not Acceptable`; unknown pages receive `404 Not Found`. HTML responses advertise their Markdown twin with an alternate link. Markdown copies link back to canonical HTML and are excluded from search indexing. The customer application is separate from these public exports. The OpenAPI schema documents a credential-protected metadata contract; it is not an API gateway. The [MCP server](https://ethicsportal.eu/developers/mcp/) is part of the customer application: an assistant reaches it only after a person approves the connection. --- # Integration changelog Source: https://ethicsportal.eu/developers/changelog/ ## Versioning and compatibility The API has one current contract. URLs carry no version. The `info.version` field in the [OpenAPI schema](https://ethicsportal.eu/openapi.json) identifies the contract with semantic versioning: the minor version rises for additions and the major version for a breaking change. Every change is listed on this page with its date. ### Changes that do not break clients These can ship at any time, recorded here: * New endpoints, operations and scopes. * New optional request fields. * New response fields, including fields inside nested objects. * New values in fields that list a fixed set of values, such as a report `status`. * New webhook event types, and new fields in webhook payloads. * New error codes, response headers and `_url` links. ### Build your client to tolerate them * Ignore response and webhook fields you do not recognise. * Handle an unknown value in a fixed-value field without failing. * Acknowledge webhook events whose `type` you do not handle with a `2xx` response, and ignore them. * Follow `_url` links and `Link` headers instead of building URLs. * Treat IDs and cursors as opaque strings. ### Breaking changes A breaking change removes or renames a field, endpoint or event type, changes a field’s type or meaning, makes an optional request field required, or asks for a different scope. The confidential-data boundary never changes: no version adds report content to the API. We avoid breaking changes. If one is needed: 1. The new behaviour ships next to the old one. 2. The old one is marked deprecated in the OpenAPI schema, and its responses carry `Deprecation` and `Sunset` headers ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745), [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)). 3. We email everyone whose tokens still use it: the owner of a personal token, or the administrators of the organisation that owns an organisation token. 4. It keeps working for at least six months after that email. If breaking changes ever become frequent, new behaviour will be opt-in through an `EthicsPortal-Version` date header. Requests without the header will keep the contract they use today. ## MCP server 10 November 2026\. An AI assistant can connect to EthicsPortal through the [MCP server](https://ethicsportal.eu/developers/mcp/) and call the API’s operations as tools, after a person approves the connection with OAuth. It started as a pilot for chosen organisations. Existing API tokens and webhooks work as before. In the audit trail, an entry written through a connection has the actor kind `mcp`. 11 November 2026\. A person with no account or no organisation can [start from an assistant](https://ethicsportal.eu/developers/mcp/#start-without-an-organisation): they sign up on the consent screen, and the assistant creates their first organisation. The same day, the MCP server opened to every organisation that has API access. ## 1.1.0: feedback from any token, and a feedback link in every error Released on 10 November 2026. * [Product feedback](https://ethicsportal.eu/developers/administration/#product-feedback) needs no scope. Any valid token can send it, and an organisation token can send it too. The `feedback:write` scope no longer exists. It was removed from the tokens that held it, and their other scopes are unchanged. * Every error response carries `error.feedback_url`, the address of the feedback operation. [Errors](https://ethicsportal.eu/developers/pagination-and-errors/#errors). ## 1.0.1: audit trail entries appear after a minute Released on 10 November 2026\. Both audit trail endpoints now return an entry once it is a minute old. Before, an entry whose write finished late could appear behind a timestamp a collector had already passed, and resuming with `created_after` skipped it. [Read the audit trail](https://ethicsportal.eu/developers/administration/#read-the-audit-trail). ## Initial contract Version 1.0.0, released on 10 November 2026\. The first API and webhook contract, available to every organisation. * Organisation, whistleblowing channel and report metadata reads. * Personal tokens that act with their owner’s current role in every organisation, and organisation tokens that belong to one organisation and keep working when staff change. * Fixed scopes per token: `reports:read`, `channel:read`, `channel:write`, `webhooks:read`, `webhooks:write`, and `organizations:create` for personal tokens. A missing scope returns `insufficient_scope` and names it in `WWW-Authenticate`. * Lists are objects with `data` and `next_cursor`, so they can gain fields without breaking clients. * `Idempotency-Key` on every POST makes retries after a lost response safe. * The audit trail, members, invitations and organisation renaming, with `audit_log:read`, `members:read`, `members:write` and `organizations:write`. * Document downloads (`documents:read`), billing status (`billing:read`), and accepting or declining your own invitations with a personal token (`invitations:respond`). * Product feedback with a personal token (`feedback:write`). * Subscription checkout and billing portal links (`billing:write`), completed by a person on Stripe’s pages. * `ETag` on every JSON read with `If-None-Match` support, and `_url` links so clients can navigate from `/identity.json` without building URLs. * Organisation creation and channel configuration with strict JSON writes and field-only validation errors. New organisations still need their DPA accepted in the application. * Organisation-owned, signed lifecycle and deadline webhooks, with retries, stable event IDs and scheduled retry times in delivery history. Everything on the webhook screen is also available through the API. * Token activity with separately sampled successful requests and refusals, plus a limit of 20 active tokens per user and per organisation. * Webhook failure warnings with administrator notifications. Failures do not automatically disable webhooks. * Explicit exclusion of report content, identities, attachments, messages, notes, category, closure outcome and retaliation.