Developers

Connect your system to Membership so it can check who is a member of an organization — without copying member data into your own database.

Overview

Organizations manage their members in Membership. Other systems often need to know whether a person is a current member: a competition system pricing an entry, a booking system checking access, a training log verifying eligibility. The Connections API answers exactly that question and nothing more.

An organization administrator authorizes your system once, and in return you can:

  • Read the connected organization's public identity (name, slug, member count)
  • Ask whether a given email address belongs to a current member of that organization
  • Read that member's membership type and whether their current season fee is settled

The API is read-only and scoped to a single organization. It never returns names, addresses, phone numbers, or fee amounts, and it cannot modify anything in Membership.

If you want members to sign in to your service themselves and say who they are as they do, use Sign in with Membership instead of the Connections API. The member then agrees to share their details, and your service receives their name, email address and membership from them directly.

How pairing works

Pairing is deliberately built so that no long-lived secret ever passes through a browser, an email, or a chat message. The administrator handles only a short-lived code; the real API key is exchanged between your server and ours.

  1. An owner or admin of the organization opens their organization settings in Membership and generates a pairing code. It looks like ABCD-EFGH, is valid for 15 minutes, and can be used once.
  2. The administrator enters that code in your system, wherever you put integration settings.
  3. Your server — not your frontend — calls POST /ext/connections/claim with the code and a description of itself and the resource being linked.
  4. Membership verifies the code, creates the connection, and returns an API key together with the organization's identity. This is the only time the key is ever shown.
  5. You store the key encrypted, then use it for all later calls. The administrator sees your system in the organization's connection list and can revoke it at any time.

Because the administrator generates the code inside Membership, holding a valid code proves the person authorizing the connection really administers that organization. You never ask a user for Membership credentials, and Membership never needs to know your users' passwords.

Base URL and authentication

All endpoints live under https://api.membership.fi/api/v1. Send the API key as a bearer token on every call except the claim itself:

Authorization: Bearer mepk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Keys are prefixed mepk_, carry 256 bits of entropy, and are scoped to one organization and one linked resource. This is a server-to-server API: browser requests are rejected by CORS, and shipping a key to a client would hand an attacker a read token for that organization's membership status. Keep it on your backend, encrypted at rest.

Endpoints

The API describes itself. Everything below is read from /api/v1/openapi.json, an OpenAPI 3.1 document generated from the schemas the API validates requests against and served by the running deployment. It needs no key to read, and you can point a client generator straight at it.

Because it comes from the code that enforces the contract rather than from a page somebody maintains, it cannot describe an endpoint that no longer works that way.

Reading the API description...

Responses and errors

Every JSON response is wrapped in an envelope. Successful calls return { "success": true, "data": ... }; failures return { "success": false, "error": { "code", "message" } }.

StatusMeaningWhat to do
400Invalid pairing code, or a request field failed validationAsk for a new code; fix the payload. Do not retry unchanged.
401Missing, unknown, rotated, or revoked API keyStop calling, mark the connection as needing attention, and prompt for a new pairing code.
429Rate limit exceededBack off and retry later; cache results to avoid repeat calls.
5xxMembership is unavailableRetry with backoff and apply your documented fallback. Never block a user indefinitely on this call.

Key lifecycle

There are exactly two ways a key stops working, and both surface as HTTP 401:

  • Re-pairing. Claiming a new code with the same systemKey and externalId rotates the key on the existing connection. The previous key stops working immediately, so a re-pair never leaves an extra live key behind. This is how a user recovers a broken connection.
  • Revocation. An organization administrator can revoke your access from their settings at any moment, and you can revoke your own key with DELETE /ext/connection.

Treat a 401 as a normal state, not a crash. Show the user that the connection needs reconnecting and how to fix it, and stop making calls until they do — retry loops against a revoked key just burn your rate limit.

Implementation checklist

  • Claim codes and store keys on your server only; never expose a key to a browser or app.
  • Encrypt keys at rest and keep them out of logs, error reports, and API responses.
  • Restrict who can pair and unpair to administrators of the resource being linked.
  • Use a stable systemKey, and send an externalUrl so staff can see what they authorized.
  • Send only the email address of a person actually interacting with that organization's resource. This API is not a lookup tool for arbitrary addresses.
  • Cache verification results briefly — minutes to hours — rather than calling on every page view.
  • Choose and document a fallback for when Membership is unreachable: fail open (treat as non-member and charge the standard price) or fail closed (block), but never leave it implicit.
  • Call DELETE /ext/connection when the user disconnects, then delete the key.
  • Handle 401 by prompting for a new pairing code instead of retrying.

Privacy

Verification sends one email address to Membership and returns membership status. Both sides are processing personal data, so as the implementer you should:

  • Tell your users in your privacy policy that membership status is checked against Membership, and why.
  • Store only what your feature needs — a boolean and a timestamp is usually enough.
  • Keep the connection scoped to the resource it was authorized for; do not reuse one organization's key elsewhere in your system.

Organizations remain the controllers of their member data. Their administrators can see every connected system, when it was last used, and revoke it without contacting you.

Worked example

Pair once, then verify a member:

# 1. Claim the code the administrator gave you (server-side)
curl -X POST https://api.membership.fi/api/v1/ext/connections/claim \
  -H 'Content-Type: application/json' \
  -d '{
    "code": "ABCD-EFGH",
    "systemKey": "example-system",
    "systemName": "Example System",
    "externalId": "club-42",
    "externalName": "Example Club",
    "externalUrl": "https://example.com/clubs/example-club"
  }'

# 2. Store data.apiKey encrypted, then verify membership
curl -X POST https://api.membership.fi/api/v1/ext/members/verify \
  -H "Authorization: Bearer $MEMBERSHIP_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "person@example.com" }'

The same flow in TypeScript, with the failure modes handled:

const BASE_URL = 'https://api.membership.fi/api/v1';

export async function verifyMember(apiKey: string, email: string) {
  const response = await fetch(`${BASE_URL}/ext/members/verify`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }),
  });

  if (response.status === 401) {
    // Revoked or rotated: ask the user to pair again
    throw new ConnectionNeedsReconnect();
  }
  if (!response.ok) {
    // Unavailable or rate limited: apply your documented fallback
    throw new MembershipUnavailable(response.status);
  }

  const { data } = await response.json();
  return data; // { isMember, membershipType, memberSince, currentSeason }
}

Sign in with Membership (OpenID Connect)

An association's website and other services can sign a member in with their Membership account. Signing in follows the OpenID Connect standard, so any library or plugin that supports it works without code written for Membership. The service learns who signed in and their membership in the association that added the service. The password never passes through the service, and the member agrees to share their details once per service.

WordPress

For WordPress sites there is a ready plugin, Membership.fi. It handles signing in, pages and files for members only or for chosen groups, and ending access when a membership ends. Updates come from membership.fi with WordPress's usual updates. Download the plugin and see the setup guide (in Finnish). For a service on another platform, the settings any OpenID Connect library needs are below.

Getting started

  1. An administrator of the association adds the service in Membership under Settings, Connections, Sign in with Membership, and gives its redirect URI. The redirect URI must be exactly the one the service sends. It always starts with https://, except on a development machine (http://localhost).
  2. Membership shows the client ID and client secret. The secret is shown only once.
  3. Enter them in the service's OpenID Connect settings together with the discovery URL below.

Settings

Discoveryhttps://api.membership.fi/oidc/.well-known/openid-configuration
Issuerhttps://api.membership.fi/oidc
Authorizationhttps://api.membership.fi/oidc/auth
Tokenhttps://api.membership.fi/oidc/token
Userinfohttps://api.membership.fi/oidc/me
JWKShttps://api.membership.fi/oidc/jwks
Response typecode (authorization code flow)
Client authenticationclient_secret_basic or client_secret_post
SigningRS256
PKCES256, optional but recommended

Claims

Ask for the scope openid profile email membership tags. The claims are in both the ID token and the userinfo response.

ScopeClaims
openidsub: a permanent identifier that stays the same when the member changes their email address. Identify users by this.
profilename, given_name, family_name, locale
emailemail, email_verified
membershiporganization_id, organization_name, membership_status (always active), membership_role (owner, admin, viewer or member), membership_type, member_number, member_since (YYYY-MM-DD) and groups: the names of the groups or sections the member belongs to.
tagstags: the names of the member's tags that the association has chosen to share with this service, such as Verkkosivujen ylläpitäjä. Tags are otherwise visible only to the association's admins, so the service is told none of them until the association picks them in the service's settings.

Missing values are left out rather than sent as null. Groups and tags are identified by name, so if the association renames one, update the name in the service's settings too.

The member agrees to the tags scope separately. When the association shares the first tag with a service, members are asked again on their next sign-in, and until then no tags claim is sent at all.

Who can sign in

Only approved members of the association. Anyone else is told on Membership's page why signing in did not work, and the service receives access_denied at its redirect URI. The member's memberships in other associations are never visible to the service.

When a membership ends

Every service receives a refresh token valid for 30 days. Refreshing it returns a new ID token with current claims. If the membership has ended or the member has withdrawn their permission, the refresh fails with invalid_grant. Sign the user out of the service then. We recommend refreshing at least once a day if the service has members-only content. Using a refresh token always takes the client secret as well.

Sessions

Membership checks on every sign-in who is signed in to Membership at that moment. A member who is already signed in and has agreed before goes straight back to the service. That is also why prompt=none always returns login_required. Signing out of the service does not sign the member out of Membership.

Feedback and requests

Have feedback on the API, or need a capability it does not cover yet? Send it through the feedback form — it needs a Membership account, and requests from integrators are read alongside everything else. Feedback goes only to the team; feature wishes are published once reviewed, so others can vote on them.