Connect with Geena

Connect with Geena

One button in your app opens a popup on Geena’s origin; the user logs in (email + one-time code — first-time users are provisioned on the spot), sees one consent screen with your manifest’s asks and terms, and the popup closes. Your backend then exchanges a short-lived code for tokens and the connection’s request_id. Returning users with a live consent skip the screen entirely — the popup flashes and closes.

The flow is standard OAuth 2.0 authorization-code with PKCE (S256, mandatory), plus one Geena-specific parameter: manifest_id, which tells the ceremony what you are asking for.

Two ways a connection starts

Every manifest carries an initiation policy — organization-initiated, recipient-initiated, or both — chosen when it is authored:

  • Recipient-initiated (this page). Your button carries manifest_id; the user’s click is the request. This is the lane for self-service signup and most product flows, and it needs a manifest whose policy allows recipient initiation. An organization-only manifest refuses the ceremony before any screen renders — except for a user who already holds an active connection, because a returning user’s ceremony is a login, which the policy never gates.
  • Organization-initiated. Your team creates the request against a chosen person in the Geena dashboard (Manifests → send), and Geena invites them by email; they accept on Geena’s own pages, no integration involved. Your app meets the connection afterwards: run a pure-login ceremony and list GET /partner/v1/requests to discover it — or, if your systems recorded the request id at send time, pass request_id in the authorize URL to resume that exact invitation in-app.

Both lanes end in the same place: an active connection addressed by request_id, indistinguishable on every other route.

1. The authorize URL

Open a popup (synchronously, inside the click handler — popup blockers) to:

GET https://api.test.geena.eu/oauth/authorize
Parameter Value
response_type code
client_id your registered client id
redirect_uri a page on one of your registered origins (exact origin match)
state random value you verify on return (CSRF)
code_challenge BASE64URL(SHA-256(verifier)) — PKCE S256, required
code_challenge_method S256
manifest_id the manifest this button asks consent for

manifest_id is deployment configuration of your app, next to your client_id: publish the manifest in the Geena dashboard and copy its id. Connections are one per (user, organization, manifest) — the same button on the same user resumes the existing connection instead of duplicating it, so the call is safely repeatable. A different product flow with a different ask is simply a different manifest_id on a different button.

Info

Omitting manifest_id runs a pure login ceremony: the user consents to the identity connection only and no data connection is created. You can also pass request_id instead, to resume a specific invitation your organization sent by email — useful only if you carry Geena request ids in your own records; most integrations should let manifest_id do the work.

2. Popup + PKCE snippet

function base64url(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

async function connectWithGeena({ clientId, manifestId, redirectUri }) {
  const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
  const digest = await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(verifier));
  const challenge = base64url(new Uint8Array(digest));
  const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
  sessionStorage.setItem("geena_connect", JSON.stringify({ verifier, state }));

  const url = new URL("https://api.test.geena.eu/oauth/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: clientId,
    redirect_uri: redirectUri,
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
    manifest_id: manifestId,
  }).toString();

  const popup = window.open(url, "geena-connect", "popup,width=480,height=720");

  return new Promise((resolve, reject) => {
    window.addEventListener("message", function onMsg(event) {
      if (event.origin !== window.location.origin) return;
      if (!event.data || event.data.type !== "geena:connected") return;
      window.removeEventListener("message", onMsg);
      const saved = JSON.parse(sessionStorage.getItem("geena_connect"));
      if (event.data.error) return reject(new Error(event.data.error));
      if (event.data.state !== saved.state) return reject(new Error("state mismatch"));
      resolve({ code: event.data.code, verifier: saved.verifier });
    });
  });
}

On mobile, fall back to a full-page redirect to the same URL — the flow is identical, only the window management differs.

3. The landing page on your origin

The browser returns to your redirect_uri with ?code=...&state=... (or ?error=access_denied if the user declined). Serve a tiny page there that hands the result to the opener and closes:

<!doctype html>
<script>
  const p = new URLSearchParams(window.location.search);
  if (window.opener) {
    window.opener.postMessage({
      type: "geena:connected",
      code: p.get("code"),
      state: p.get("state"),
      error: p.get("error"),
    }, window.location.origin);
  }
  window.close();
</script>

4. Exchange the code on your backend

Send the code and PKCE verifier to your own backend; the exchange requires your client_secret and must never happen in the browser. The code is single-use and expires after 5 minutes; redirect_uri must repeat the value used at authorize.

curl -s https://api.test.geena.eu/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=your-app \
  -d client_secret=$GEENA_CLIENT_SECRET \
  -d code=$CODE \
  -d code_verifier=$VERIFIER \
  -d redirect_uri=https://app.example.com/geena/callback
{
  "access_token": "eyJhbGciOiJFZERTQSIs…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "0f3c…",
  "scope": "offline_access profile",
  "request_id": "6f9b2c9e-6a4e-4a44-9d5e-2f0f6f2a9b11"
}

request_id is the connection this ceremony minted or resumed — store it with your user record; every /partner/v1 route is addressed by it. (It is absent on pure-login ceremonies; you can always re-discover connections via GET /partner/v1/requests.)

5. Token lifecycle

  • Access tokens live 15 minutes. Refresh silently with the refresh_token grant; the partner session rolls forward on each refresh, so an actively used integration does not expire.
  • Refresh tokens rotate. Every refresh response carries a new refresh_token; always persist the latest. Presenting a superseded or revoked refresh token is treated as theft evidence: the whole token family and its session are revoked, and you must reconnect through the ceremony.
curl -s https://api.test.geena.eu/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=your-app \
  -d client_secret=$GEENA_CLIENT_SECRET \
  -d refresh_token=$REFRESH_TOKEN
  • interaction_required on any token call means the user’s consent was revoked (they can do this from their Geena dashboard at any time) — open the ceremony popup again; nothing else will mint tokens.
  • Disconnect cleanly with RFC 7009 revocation — present any of your refresh tokens and the whole grant is torn down (consent, sessions, token families):
curl -s https://api.test.geena.eu/oauth/revoke \
  -d client_id=your-app \
  -d client_secret=$GEENA_CLIENT_SECRET \
  -d token=$REFRESH_TOKEN
Tip

The token endpoints are per-IP rate limited. Exchange and refresh from your backend, cache access tokens for their full 15 minutes, and never poll /oauth/token.