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 your use case needs more than a membership check, ask the organization to export the data instead.

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

POST /ext/connections/claim

Exchanges a pairing code for an API key. Unauthenticated — the code is the credential — and rate limited to 20 attempts per 15 minutes per IP address. Your system identifies itself here.

FieldTypeDescription
codestringThe pairing code from the administrator. Case and separators are normalized, so abcd-efgh and ABCDEFGH both work.
systemKeystringA stable lowercase slug identifying your system, 2–50 characters matching ^[a-z0-9][a-z0-9-]*$. Never change it: it identifies the connection when re-pairing.
systemNamestringYour system's display name, shown to organization staff. Max 100 characters.
externalIdstringYour stable identifier for the resource being linked — the club, team, or branch on your side. Max 100 characters.
externalNamestringDisplay name of that resource, shown to staff. Max 200 characters.
externalUrlstring, optionalLink back to the resource so staff can see what they connected. Max 500 characters.

A successful claim returns HTTP 201:

{
  "success": true,
  "data": {
    "apiKey": "mepk_...",
    "organization": {
      "id": "clx...",
      "slug": "example-club",
      "name": "Example Club ry",
      "shortName": "EC",
      "profileImageUrl": null,
      "membershipPolicy": "APPROVAL_REQUIRED"
    }
  }
}

Every failure — wrong code, expired code, already-used code — returns the same HTTP 400 with the message Invalid or expired pairing code, so the endpoint cannot be used to discover which codes exist. Ask the administrator for a fresh code and try again.

GET /ext/organization

Returns the connected organization and a summary of the connection. Use it to confirm a key still works and to display the linked organization in your own settings screen.

{
  "success": true,
  "data": {
    "organization": {
      "id": "clx...",
      "slug": "example-club",
      "name": "Example Club ry",
      "shortName": "EC",
      "profileImageUrl": null,
      "membershipPolicy": "APPROVAL_REQUIRED",
      "memberCount": 120
    },
    "connection": {
      "externalId": "your-club-id",
      "externalName": "Example Club",
      "connectedAt": "2026-08-08T09:12:00.000Z"
    }
  }
}

POST /ext/members/verify

The main endpoint. Send one email address; get back membership status. Matching is by email, case-insensitive. Rate limited to 120 requests per minute.

// Request
{ "email": "person@example.com" }

// Response
{
  "success": true,
  "data": {
    "isMember": true,
    "membershipType": "Adult",
    "memberSince": "2024-01-15T00:00:00.000Z",
    "currentSeason": {
      "id": "clx...",
      "name": "2026",
      "feeStatus": "PAID",
      "feePaid": true
    }
  }
}

How to read the response:

  • isMember is true when the person has an approved, active membership in the connected organization. An unknown email address and a rejected, banned, pending, or removed membership all return false — the endpoint never reveals which.
  • membershipType is the organization's own category name, for example "Adult" or "Junior". Organizations define these freely, so treat the value as a label, not an enum.
  • currentSeason describes the organization's active membership season, or is null when no season is currently running.
  • feeStatus is one of UNBILLED, INVOICED, PAID, WAIVED, or NONE when no fee has been generated for this member yet. feePaid is a convenience flag, true for PAID and WAIVED.

Decide explicitly which of these facts your feature requires. "Is a member" and "is a member who has paid this season" are different rules, and organizations invoice on very different schedules — gating on feePaid in January may exclude members whose invoices simply have not been sent yet.

DELETE /ext/connection

Revokes the key you are calling with and returns HTTP 204. Call this when a user disconnects the integration on your side, so no usable key is left behind, and delete your stored copy.

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 }
}

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.