> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fitsociety.io/llms.txt
> Use this file to discover all available pages before exploring further.

# External client access

> Manage expiring app and client-AI access through OAuth, independently of a payment provider.

This contract is for company environments explicitly configured for externally
managed app access. Existing manually managed members keep their current access
policy. An external payment is not a FITsociety membership invoice.

## Company opt-in and scopes

A company administrator or authorized staff member creates a Public API OAuth
client through `POST /app/v1/integrations/public-api/clients`, using their coach
token and the `integrations.api_credentials.create` permission. The company must
have Public API provider access. The company is derived from that authenticated
context; do not pass a company ID.

This example enables client creation and app/AI grant synchronization:

```bash theme={null}
curl -X POST \
  'https://api.fitsociety.io/app/v1/integrations/public-api/clients' \
  -H "Authorization: Bearer $FITSOCIETY_COACH_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "External member access",
    "externalAppAccessEnabled": true,
    "defaultAssignedCoachId": "66f7b8b1e13c8d25f4d3d90a",
    "scopes": [
      "clients:read",
      "clients:write",
      "client_access:read",
      "client_app_access:write",
      "client_ai_access:write"
    ]
  }'
```

`externalAppAccessEnabled` defaults to `false`. Set it at OAuth client creation;
`client_app_access:write` must be among that client's configured scopes when the
option is `true`. Public API callers cannot enable it themselves. Add only the
workout scopes and explicit health consent required by your programme operations;
see [authentication and consent](/public-api/authentication#consent-gated-scopes).

Store the returned `clientId` and one-time `clientSecret` for the OAuth token
exchange. To rotate the secret later, use the returned `_id` with
`POST /app/v1/integrations/public-api/clients/{id}/rotate-secret` through the
company management API. Secret rotation preserves reference and grant ownership;
creating a replacement OAuth client creates a separate namespace.

For a new company-client relationship, the required external app-access policy
commits atomically with the relationship before any creation or linking email
runs. The same rule applies when linking an existing global account to a new
company relationship. Existing manual relationships are never silently taken
over. Existing creation/linking emails may arrive before an active grant has
been synchronized. An invitation alone does not provide app access; respect the
existing email behavior and do not send a duplicate invitation. Announce paid
activation only after the effective-access read confirms the requested rights.

* `client_access:read`: read owned grants and current effective access.
* `client_app_access:write`: manage the `app` capability.
* `client_ai_access:write`: manage the `client_ai` capability.
* Client and workout operations retain their existing scopes and consent gates.

The company must still have the client-AI licence and enabled supporting modules.
An AI capability authorizes the existing permitted client-AI features; it does
not add REST endpoints for AI chat, nutrition recognition or workout generation.
Every actual feature keeps its own licensing and module checks.

## Grant status versus client status

An external grant authorizes capabilities for one company-client relationship.
Its `status` is separate from the client's relationship, membership and portal
statuses. Writing a grant does not activate, pause, archive or otherwise change
those lifecycle fields. See [Client statuses and app access](/public-api/clients#client-statuses-and-app-access)
for their values and supported mutations.

| Grant status | Meaning |
| :- | :- |
| `active` | The grant can contribute its capabilities during its valid interval, subject to company policy and licensing. |
| `revoked` | This grant no longer contributes access, regardless of its dates. Independent valid rights remain separate. |

There is no stored `expired` or `scheduled` grant status. An `active` grant can
be in the future or already expired. FITsociety checks its dates when evaluating
access; it does not rewrite the grant status when time passes.

For example, `relationship.status: "active"`, portal status `enabled` and grant
status `active` can still produce `effective.app: false` after `validUntil` in
an externally managed relationship. A company block or portal restriction can
also prevent access during a grant's valid interval. Always confirm the current
effective result instead of deriving it from one status field.

A deleted or missing global client account always has `effective.app: false`
and `effective.clientAi: false`. A new active revision then returns
`403 access.client_unavailable`. Owned grants remain readable and revocable for
reconciliation; an exact stored-grant revision replay does not reactivate the account.

## Create, renew or revoke a grant

Use `PUT /public/v1/clients/{clientId}/access-entitlements/{externalId}`.
The identifier is opaque, at most 200 characters, and belongs to the authenticated
API client within its company. No payment provider prefix or billing cadence is
required. Every write also requires an `Idempotency-Key`.

```json theme={null}
{
  "capabilities": ["app", "client_ai"],
  "status": "active",
  "validFrom": "2026-10-12T10:00:00Z",
  "validUntil": "2027-10-12T10:00:00Z",
  "revision": 1
}
```

Dates must be finite UTC instants ending in `Z`, with `validFrom < validUntil`.
Access applies at `validFrom <= now < validUntil`. Expired intervals may be
recorded for reconciliation; they do not grant current access. Renew by updating
the same identifier with a newer positive safe-integer revision and a new paid
interval. Revoke with `status: "revoked"` and a newer revision. Retained records
prevent an older active message from restoring access. A relationship supports
at most 100 retained entitlement records; renew the same source entitlement
rather than creating a new record for each payment period.

Permissions are checked for every existing and requested capability, including
removal and revocation. A caller with only app-write scope cannot remove an AI
capability from an existing combined grant. A new active app+AI write is rejected
entirely with `403 access.ai_unavailable` when AI is unavailable; the previous
grant is unchanged. Revocation remains possible without an AI licence.

Same revision and normalized content confirms the existing grant. An older
revision returns `409 access.stale_revision`; conflicting content at the same
revision returns `409 access.revision_conflict`. Secret rotation keeps the same
ownership identity. A new OAuth client is a separate source and requires an
explicit ownership migration; do not replace one to rotate its secret.

## Confirm effective access

Read `GET /public/v1/clients/{clientId}/access-entitlements` after synchronization.
The response includes only this API client's grants and an `effective` summary:

```json theme={null}
{
  "entitlements": [],
  "effective": {
    "app": false,
    "clientAi": false,
    "externalAppAccessRequired": true
  }
}
```

The example shows a new managed relationship before its first grant. Responses
use the standard `{ data, meta }` envelope. A successful stored-grant write does
not guarantee that the full product is currently accessible: company blocks,
portal policy, feature settings and licence changes still apply. HTTP idempotency
replays contain the original response snapshot; GET returns current state.

For externally managed relationships, an administrator company block also denies
login, company selection and existing-session access without modifying the grant.

Expiry is evaluated on existing authenticated sessions and access checks. It
needs neither a webhook nor a scheduled database update. Other valid grants and
manual AI rights remain independent. Revoking one integration's grant cannot
remove another source's access. There is no guaranteed grant-expiry webhook in
this version; integrations should read current state when needed.

## Recoverable client and programme synchronization

Use [durable external references](/public-api/external-references) for client,
exercise and workout-plan identity. A typical server-side flow is:

1. Resolve the client by owned external ID; optionally find an existing client by exact email and bind its reference.
2. Create or link a new client through the existing client API when needed.
3. Resolve controlled exercise IDs and create the assigned programme through the existing workout API.
4. Synchronize the paid access interval and read back effective app and AI access.
5. Announce activation only when all requested capabilities are effective.

Persist each completed step in the integrating system. Programme metadata,
days, exercises and supersets are separate existing resources; creating a plan
does not implicitly import arbitrary nested content. Do not invent exercise
classification data to bypass required validation. Unchanged renewals reuse the
same programme and preserve training logs.

Dynamic capability permissions and the current API-client opt-in are checked before an HTTP idempotency response can replay. The stored grant is checked again during each actual mutation.


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