Skip to content

OAuth: organizer connects your integration

The flow that issues an installation token — server-to-server credentials your integration uses to read event data on behalf of a specific event.

This is the OAuth flow you'll implement first, regardless of what your integration does. Even a participant-facing app (which also implements Login with Revento) can only be reached by participants once an organizer has connected it to their event using this flow.

When to use this flow

  • You need to read event data, participants, applications, or program.
  • Your integration acts on behalf of "this specific event," not on behalf of an individual user.
  • Token requests come from your servers, not from a browser.

If your integration only signs participants in (and never reads bulk event data), you might get away with Login with Revento alone. In practice, almost everyone needs both.

Sequence

sequenceDiagram
    autonumber
    actor O as Organizer
    participant B as Organizer's browser
    participant Y as Your backend
    participant R as Revento

    O->>B: Click "Connect {your integration}"<br/>in the integrations panel
    B->>Y: GET /connect (your route)
    Y->>Y: Generate PKCE verifier + challenge<br/>Generate CSRF state token
    Y->>B: 302 → Revento /oauth/authorize<br/>(client_id, redirect_uri, scope, state, code_challenge, event_id)
    B->>R: GET /oauth/authorize?...
    R->>O: Render consent screen<br/>(scopes, organization name, event name)
    O->>R: Click "Authorize"
    R->>B: 302 → your redirect_uri<br/>?code=AUTH_CODE&state=...
    B->>Y: GET /callback?code=...
    Y->>Y: Verify state matches what was sent
    Y->>R: POST /oauth/token<br/>(grant_type=authorization_code, code, code_verifier, client_secret)
    R-->>Y: 200 OK<br/>{access_token, refresh_token, event_id, organization_id, scope}
    Y->>Y: Store tokens, bound to (event_id, organization_id)
    Y-->>B: Show "Connected!" UI

Authorization request

Redirect the organizer's browser to:

GET https://auth.revento.example/oauth/authorize?
    response_type=code
    &client_id={your client_id}
    &redirect_uri={your registered redirect_uri}
    &scope={space-separated scopes}
    &event_id={the event being connected}
    &state={CSRF token}
    &code_challenge={PKCE challenge}
    &code_challenge_method=S256

Parameters

Parameter Required Description
response_type yes Always code. We don't support implicit or other grant types.
client_id yes Your integration's client_id, issued at registration.
redirect_uri yes Must exact-match one of the redirect URIs you registered. No wildcards, no path-prefix matching.
scope yes Space-separated list of scopes you're requesting. Must be a subset of the scopes declared in your integration manifest.
event_id yes The event the organizer is connecting your integration to. Revento uses this to scope the consent screen and bind the resulting token.
state recommended CSRF token. Returned untouched on the redirect. Verify it matches before exchanging the code.
code_challenge yes PKCE challenge: base64url-encoded SHA-256 of your code_verifier.
code_challenge_method yes Always S256. We don't support plain.
prompt optional Only consent is supported. Forces the consent screen even if the organizer has previously consented. Other values (login, none, select_account) return invalid_request. Default behavior already re-shows the consent screen for fresh connections, so this is rarely needed.

Validation rules

  • Redirect URI must exact-match. https://example.com/cb and https://example.com/cb/ are different URIs. Register every variant you actually use.
  • Scopes must be a subset of your manifest. Requesting participants.read when your manifest only declares event.read returns invalid_scope.
  • PKCE is required. Both for public and confidential clients. No exceptions.
  • The organizer must have permission to connect. If the organizer hitting /oauth/authorize lacks integration.manage or event.owner on the event, they see an error page rather than the consent screen.

Possible error redirects

If the authorization request can't proceed, Revento either renders an error page (for user-facing errors) or redirects back to your redirect_uri with error parameters (for protocol-level errors).

error When
invalid_request Malformed request, missing required parameter, bad PKCE format.
unauthorized_client client_id is unknown or your integration has been suspended.
invalid_scope A requested scope isn't declared in your manifest.
access_denied The organizer clicked "Cancel" on the consent screen.
server_error Internal error during authorization. Surface a "try again later" message; the failure is logged on our side and the request_id will help if you escalate.

What the organizer sees

The consent screen renders in the organizer's locale (Polish or English; copy below is the English version — the Polish locale shows equivalent translations) with this structure:

  • Headline: "{your integration name} is requesting access to {event name} data"
  • Publisher line: "Publisher: {your publisher name}"
  • Scope list: each scope as a row with its description, marked "required" or with an opt-in checkbox (optional)
  • Data scope clause: "Only within event {event name}. No data modification."
  • Responsibility note: "Your organization {organization name} is responsible for data shared with the integration."
  • Actions: "Authorize" (primary) and "Cancel" (secondary)

The organizer can decline optional scopes by unchecking them. Required scopes are all-or-nothing — granted or the connection doesn't happen.

Token exchange

After the organizer approves, Revento redirects to your redirect_uri with:

GET {your redirect_uri}?code=AUTH_CODE&state=YOUR_STATE

You verify state matches what you sent, then exchange the code:

POST /oauth/token HTTP/1.1
Host: auth.revento.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code={the code from the redirect}
&redirect_uri={the same redirect_uri you used in /authorize}
&client_id={your client_id}
&client_secret={your client_secret}
&code_verifier={the PKCE verifier matching the challenge}

Successful response:

{
  "access_token":   "rev_install_8h3k2m...",
  "refresh_token":  "rev_refresh_a92cf1...",
  "token_type":     "Bearer",
  "expires_in":     3600,
  "scope":          "event.read participants.read program.read",
  "event_id":       "evt_abc123",
  "organization_id": "org_xyz789",
  "integration_id": "int_yourapp"
}

Response fields

Field Description
access_token The installation token. Use as Authorization: Bearer ... on every API call. Lifetime ~1 hour.
refresh_token One-time-use refresh token. Lifetime: 90-day sliding window, 1-year hard cap. See Refresh tokens.
token_type Always Bearer.
expires_in Seconds until the access token expires. Refresh before this hits 0.
refresh_expires_in Seconds until the refresh token expires. Refreshing extends this — see refresh tokens.
scope The scopes the organizer actually granted. May be a subset of what you requested if optional scopes were declined.
event_id The event this token is bound to.
organization_id The organization this token is bound to.
integration_id Your integration's stable id (matches your client_id context).

Token-exchange errors

error When
invalid_grant Code is unknown, expired (codes live ~10 minutes), already used, or doesn't match the redirect_uri / code_verifier you originally sent.
invalid_client client_id / client_secret mismatch, or client_secret was rotated and you're using the old one.
invalid_request Missing required parameter, malformed body.

Authorization codes are one-time use. If you exchange a code successfully, then exchange the same code again, the second attempt returns invalid_grant AND invalidates the tokens issued by the first exchange. Don't retry token-exchange requests on transient network errors without first checking whether the first attempt actually succeeded.

Storing the installation token

Treat both access_token and refresh_token as you would a password.

  • Encrypted at rest (your DB-level encryption, KMS-backed envelope encryption, or a secrets manager).
  • Never in env vars committed to git.
  • Never in client-side code or anywhere a customer can inspect.
  • Logged at most as last 4 chars + length for debugging.
  • Per (event, organization, integration) tuple — store event_id and organization_id alongside the token so you know what it can read.

A typical schema:

CREATE TABLE revento_installations (
  id              SERIAL PRIMARY KEY,
  event_id        TEXT NOT NULL,
  organization_id TEXT NOT NULL,
  access_token    BYTEA NOT NULL,    -- encrypted
  refresh_token   BYTEA NOT NULL,    -- encrypted
  access_expires_at  TIMESTAMPTZ NOT NULL,
  refresh_expires_at TIMESTAMPTZ NOT NULL,
  scopes          TEXT[] NOT NULL,
  status          TEXT NOT NULL DEFAULT 'active',  -- active|revoked|suspended
  connected_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (event_id, organization_id)
);

Revocation

A connection can end any of these ways. Your integration sees all of them as "the token stops working."

Initiator What happens
Organizer revokes from the integrations panel Both access and refresh tokens are invalidated immediately. A data.deletion_required webhook fires (if subscribed).
Organization loses formal status (rare) All connections across all of that org's events invalidated.
Revento suspends your integration All your tokens invalidated; we email you and the organizer.
You unpublish your integration All your tokens invalidate within one dispatch cycle. data.deletion_required fires per (event, organization).

In all three cases, your next API call returns 401 token_revoked. Your integration should:

  1. Catch the 401, mark the installation as revoked in your storage.
  2. Stop polling, stop scheduling work for this connection.
  3. Within 30 days of receiving data.deletion_required, delete cached event data on your side.
  4. Surface the disconnect to your operators if the integration's UX assumes a live connection.

Re-authorization (new scopes)

If you ship a new version of your integration that declares a scope your existing connections don't have, every existing connection enters a "re-consent required" state.

  • The organizer sees a banner in their integrations panel: "This integration requires additional permissions: {new scope}."
  • Tapping "Accept new permissions" runs them through this same flow again, with the new scope set in the consent screen.
  • The previous token continues to work for the previous scope set until the new token is issued.
  • There is no silent / banner-only path that adds scopes without consent.

See Scopes — versioning for the rules.

Common pitfalls

  • State validation skipped. Always compare the state parameter on the redirect against what you sent. Without this check, a hostile actor can complete the OAuth flow on a victim's behalf and bind your integration to an attacker-controlled session.
  • Token persisted in plaintext. Easy mistake during prototyping. Wire up encryption before you ship.
  • Catching 401 only on access tokens. Refresh tokens can also be revoked (e.g. organizer disconnect, family-revoke). A 400 invalid_grant on a refresh attempt is not a transient error — it's a permanent revocation. Don't retry; mark the installation revoked.
  • Hard-coded redirect URI. Make it config-driven so dev / staging / prod can each register their own URI without code changes.
  • Treating scope in the response as identical to your request. If the organizer declined an optional scope, the response has fewer scopes. Read it back and adapt.