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.
- 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. - The administrator enters that code in your system, wherever you put integration settings.
- Your server — not your frontend — calls
POST /ext/connections/claimwith the code and a description of itself and the resource being linked. - 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.
- 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/jsonKeys 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.
| Field | Type | Description |
|---|---|---|
code | string | The pairing code from the administrator. Case and separators are normalized, so abcd-efgh and ABCDEFGH both work. |
systemKey | string | A 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. |
systemName | string | Your system's display name, shown to organization staff. Max 100 characters. |
externalId | string | Your stable identifier for the resource being linked — the club, team, or branch on your side. Max 100 characters. |
externalName | string | Display name of that resource, shown to staff. Max 200 characters. |
externalUrl | string, optional | Link 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:
isMemberis 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 returnfalse— the endpoint never reveals which.membershipTypeis 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.currentSeasondescribes the organization's active membership season, or isnullwhen no season is currently running.feeStatusis one ofUNBILLED,INVOICED,PAID,WAIVED, orNONEwhen no fee has been generated for this member yet.feePaidis a convenience flag, true forPAIDandWAIVED.
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" } }.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Invalid pairing code, or a request field failed validation | Ask for a new code; fix the payload. Do not retry unchanged. |
| 401 | Missing, unknown, rotated, or revoked API key | Stop calling, mark the connection as needing attention, and prompt for a new pairing code. |
| 429 | Rate limit exceeded | Back off and retry later; cache results to avoid repeat calls. |
| 5xx | Membership is unavailable | Retry 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
systemKeyandexternalIdrotates 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 anexternalUrlso 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/connectionwhen 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.