Skip to content

Pagination, filters and errors

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, which only ever requests the report collection on the EthicsPortal origin.

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.

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:

Terminal window
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.

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
Terminal window
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.

{"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 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

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"

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.