> ## Documentation Index
> Fetch the complete documentation index at: https://partner-help.letsdothis.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Member access for apps

> Let a member sign in from your mobile app and read or manage their own memberships with a token that belongs to them.

An API key reaches every member your organization has, so it must stay on a
server. A mobile app with no server of its own uses **member access** instead:
the member signs in on a Let's Do This page, approves your app, and your app
gets a token that only reaches that member's own memberships with your
organization.

Member access uses the standard OAuth 2.0 authorization code flow with PKCE
([RFC 6749](https://www.rfc-editor.org/rfc/rfc6749),
[RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)). Any OAuth client library
works; on iOS and Android we recommend [AppAuth](https://appauth.io).

## Before you start

Ask us to register your app. Send us:

* your app's name as members should see it;
* its exact redirect URI;
* optionally, the organiser theme to brand sign-in with.

We give you a **client ID**, such as `yourclub-v1`. There's no client secret:
a secret can't be kept inside an app.

The theme is one of yours under **Settings → Branding** in the dashboard; send
us its address. With it, the sign-in, sign-up and password reset pages your
members pass through show your logo and name instead of Let's Do This's. Use
the same theme as your membership scheme so members see one brand throughout.
You can ask us to change it at any time, without releasing a new version of
your app.

> [!NOTE]
>
> Use a **claimed https link** as your redirect URI: an iOS Universal Link or an
> Android App Link, such as `https://yourclub.example/app/oauth`. The phone
> checks that the link belongs to your app, so members who have approved your
> app before can reconnect without being asked again. A custom scheme must be in
> reverse-domain form, such as `com.yourclub.app:/oauth`, and members are asked
> to approve your app every time it connects.

## How it works

**1. Open the connect page in the system browser**

Open `https://www.letsdothis.com/connect` with the parameters below in the
phone's browser: `ASWebAuthenticationSession` on iOS, Custom Tabs on
Android. AppAuth does this for you.

**2. The member signs in and approves**

The member signs in or creates an account, sees what your app asks for,
and taps **Allow**.

**3. Your app receives a code**

We redirect to your redirect URI with `code`, `state` and `iss`.

**4. Exchange the code for tokens**

Your app posts the code and its PKCE verifier to the token endpoint and
gets an access token and a refresh token.

**5. Call the API as the member**

Send the access token as `Authorization: Bearer <token>`.

## Authorization request

| Parameter | Value |
| - | - |
| `client_id` | Your client ID |
| `redirect_uri` | Your registered redirect URI, exactly |
| `response_type` | `code` |
| `scope` | `memberships:read`, or `memberships:read memberships:write` |
| `state` | A random value of at least 16 characters, checked when the member comes back |
| `code_challenge` | The S256 PKCE challenge |
| `code_challenge_method` | `S256` |

If the member cancels, the redirect carries `error=access_denied` instead of a
code.

## Tokens

Token requests use `application/x-www-form-urlencoded`.

**Exchange the code** (`POST https://api.letsdothis.com/v0/account/token`):

```bash theme={null}
grant_type=authorization_code
&code=<code>
&code_verifier=<verifier>
&client_id=<client id>
&redirect_uri=<redirect uri>
```

```json theme={null}
{
  "access_token": "mt_production_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "memberships:read memberships:write"
}
```

A code works once and expires after a minute.

**Refresh** with `grant_type=refresh_token`, `refresh_token` and `client_id`.
Each refresh returns a new refresh token; store it and discard the old one.
Reusing an old refresh token disconnects the member, as it suggests the token
was copied.

**Log out** with `POST https://api.letsdothis.com/v0/account/token/revoke`,
sending `token` and `client_id`. It always answers 200.

Access tokens last an hour. A refresh token expires after 60 days without
use, and in any case 12 months after the member approved your app; after that
the member connects again.

Client libraries can discover these endpoints at
`https://api.letsdothis.com/.well-known/oauth-authorization-server`.

## What a member token reaches

| Endpoint | Scope | What the member gets |
| - | - | - |
| `GET /v0/account/memberships` | `memberships:read` | Their own seats with your organization |
| `GET /v0/membership-seats/{seatId}` | `memberships:read` | Their own seat |
| `GET /v0/membership-seats/{seatId}/form` | `memberships:read` | Questions, accepted values and editability for their own seat |
| `PATCH /v0/membership-seats/{seatId}` | `memberships:write` | Updates to their own details |
| `GET /v0/membership-seats/{seatId}/entries` | `memberships:read` | The races linked to their own seat |
| `GET /v0/membership-policies/{policyId}` | `memberships:read` | A policy they hold a seat on; `includeCard` for the primary member only |
| `POST /v0/membership-policies/{policyId}/billing-portal-session` | `memberships:write` | The primary member only |
| Schemes, offerings and prices | `memberships:read` | Your organization's catalog |

Every other endpoint answers `403` with `MEMBER_TOKEN_NOT_ALLOWED`. A seat or
policy that isn't the member's answers `404`, the same as one that doesn't
exist. A secondary member asking for something only the paying member can do
gets `403` with `MEMBERSHIP_PRIMARY_MEMBER_ONLY`.

## Your app's responsibilities

* **Use the system browser, never a WebView.** Embedded browsers can read
  what the member types, and Google refuses to sign in inside them.
* **Use PKCE with S256** and a fresh verifier for every sign-in.
* **Check `state`** when the member comes back, and drop the response if it
  doesn't match.
* **Store tokens in the platform's secure storage**: the iOS Keychain or the
  Android Keystore.
* **Keep the latest refresh token** after every refresh.
* **Handle a `401`** by refreshing once, and if that fails, asking the member
  to connect again. Members can be disconnected when they reset their
  password, delete their account, or when we disable your app.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.