> ## Documentation Index
> Fetch the complete documentation index at: https://connect-oauth.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth Integration Guide

> Integrate your application with FMG Connect using OAuth 2.1 (Authorization Code + PKCE).

FMG Connect uses **OAuth 2.1 Authorization Code flow with PKCE** to let your application access **FMG** resources (contacts, groups, content) on behalf of an FMG subscriber. This guide assumes FMG has provisioned your application.

<Info>
  FMG and MRC are **separate integrations**. This guide covers **FMG** (`fmg.*` scopes). For MyRepChat texting, see the **MRC APIs → OAuth** guide — same flow, its own Client ID/Secret and `mrc.*` scopes.
</Info>

## What FMG provides

FMG provisions your application **per environment — sandbox first, then production**. Build and test against sandbox, then repeat with the production values.

| Item              | Sandbox                                                        | Production                                                   |
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
| **Client ID**     | Issued by FMG (sandbox)                                        | Issued by FMG (production)                                   |
| **Client Secret** | Issued by FMG — server-side only                               | Issued by FMG — server-side only                             |
| **Authorize URL** | `https://secure.fmgsuite.com/login?returnUrl=/oauth/consent`   | `https://secure.fmgsuite.com/login?returnUrl=/oauth/consent` |
| **Token URL**     | `https://oauth.fmgsuite.com/v1/oauth2/token`                   | `https://oauth.fmgsuite.com/v1/oauth2/token`                 |
| **API Base URL**  | `https://connect.fmgsuite.com`                                 | `https://connect.fmgsuite.com`                               |
| **Scopes**        | The `fmg.*` scopes granted to your app (see [Scopes](#scopes)) | Same                                                         |

<Warning>
  The OAuth endpoints (`oauth.fmgsuite.com`) are shared across environments, but the **Client ID and Secret differ between sandbox and production** — use each set only with the environment it was issued for. What changes per environment is the **API Base URL** (staging vs production).
</Warning>

## What you provide

You give FMG one thing up front:

| Item                | Description                                                                                                                                                                                                                 |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Redirect URI(s)** | The exact callback URL(s) in your app where FMG returns the authorization `code`. Provide one per environment (e.g. `https://app.example.com/callback` for production, `https://staging.example.com/callback` for sandbox). |

FMG registers these in its OAuth service (**Stytch**) against your Client ID. Only registered URIs are accepted at authorization time.

### Why the redirect URI matters

The redirect URI is the most security-critical value you register, because it is **where the authorization `code` is delivered**:

* After the subscriber approves consent, the authorization server sends the browser to `redirect_uri?code=...`. Whoever controls that URL receives the code and can exchange it for tokens.
* So the authorization server treats your registered URIs as a strict **allowlist**: the `redirect_uri` in the authorize request must **match a registered URI exactly** — scheme, host, port, and path. A mismatch is rejected before any code is issued.
* This closes **authorization-code interception / open-redirect** attacks — an attacker can't substitute their own callback to capture the code, because an unregistered URI never matches.
* It must be **HTTPS** and a URL your server controls (no wildcards, no fragments). Register **every** environment's callback, since sandbox and production use different values.

PKCE (Step 1) protects the code in transit; exact redirect-URI matching ensures the code is only ever *sent* to you. Both are required.

## Flow at a glance

<Steps>
  <Step title="Generate PKCE + state">
    Server-side, create a `code_verifier`, its `code_challenge` (S256), and a random `state`.
  </Step>

  <Step title="Redirect to authorize">
    Send the user's browser to the Authorize URL with your request parameters.
  </Step>

  <Step title="Handle the callback">
    FMG redirects back with `code` and `state`. Validate `state`.
  </Step>

  <Step title="Exchange code for tokens">
    Server-side `POST` to the Token URL with the `code`, `code_verifier`, and `client_secret`.
  </Step>

  <Step title="Call FMG APIs">
    Use the `access_token` as a Bearer token. Refresh it when it expires.
  </Step>
</Steps>

***

## Step 1 — Generate PKCE parameters

Generate these on your **server** and store `code_verifier` + `state` in the user's server-side session:

```text theme={null}
code_verifier  = base64url(random(32 bytes))   // 43-char string
code_challenge = base64url(SHA256(code_verifier))
state          = base64url(random(16 bytes))
```

## Step 2 — Redirect to authorization

Redirect the user's browser to the Authorize URL:

```text theme={null}
GET https://secure.fmgsuite.com/login?returnUrl=/oauth/consent
  &response_type=code
  &client_id={CLIENT_ID}
  &redirect_uri={REDIRECT_URI}
  &code_challenge={code_challenge}
  &code_challenge_method=S256
  &state={state}
  &scope={SCOPES}
```

<Warning>
  All OAuth parameters are appended as **top-level query parameters** alongside `returnUrl` — they are *not* encoded inside the `returnUrl` value:
  `https://secure.fmgsuite.com/login?returnUrl=/oauth/consent&response_type=code&client_id=...`
</Warning>

| Parameter               | Required | Value                                  |
| ----------------------- | -------- | -------------------------------------- |
| `response_type`         | Yes      | `code`                                 |
| `client_id`             | Yes      | Your Client ID                         |
| `redirect_uri`          | Yes      | Must exactly match a registered URI    |
| `code_challenge`        | Yes      | S256 challenge from Step 1             |
| `code_challenge_method` | Yes      | `S256`                                 |
| `state`                 | Yes      | Random CSRF value from Step 1          |
| `scope`                 | Yes      | Space-separated scopes assigned by FMG |

The user authenticates with FMG and approves a consent screen listing your requested permissions.

## Step 3 — Handle the callback

FMG redirects to your `redirect_uri`:

```text theme={null}
GET {REDIRECT_URI}?code={authorization_code}&state={state}
```

In your handler:

1. Compare `state` against the session value — abort on mismatch (CSRF).
2. Retrieve the `code_verifier` from the session.
3. Clear both from the session — they are single-use.

## Step 4 — Exchange the code for tokens

Server-side `POST` to the Token URL. Authenticate with your `client_secret`:

```bash theme={null}
curl -X POST https://oauth.fmgsuite.com/v1/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code={authorization_code} \
  -d client_id={CLIENT_ID} \
  -d client_secret={CLIENT_SECRET} \
  -d code_verifier={code_verifier} \
  -d redirect_uri={REDIRECT_URI}
```

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "v2.abc...",
  "scope": "openid offline_access fmg.contacts.read fmg.contacts.write"
}
```

The `refresh_token` is only returned when the `offline_access` scope is granted. Store both tokens securely (server-side session or HttpOnly cookie — never `localStorage`).

## Step 5 — Call FMG Connect APIs

Send the access token as a Bearer token:

```bash theme={null}
curl https://{API_BASE_URL}/contacts \
  -H "Authorization: Bearer {access_token}"
```

## Step 6 — Refresh the access token

When the access token expires (`401` response), use the refresh token to get a new one without sending the user through the flow again:

```bash theme={null}
curl -X POST https://oauth.fmgsuite.com/v1/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token={refresh_token} \
  -d client_id={CLIENT_ID} \
  -d client_secret={CLIENT_SECRET}
```

***

## Scopes

Request exactly the scopes FMG assigned you — requesting an unprovisioned scope fails.

| Scope                | Grants                                         |
| -------------------- | ---------------------------------------------- |
| `openid`             | Required — identity claims in the token.       |
| `offline_access`     | Returns a refresh token for long-lived access. |
| `fmg.contacts.read`  | Read the subscriber's contacts.                |
| `fmg.contacts.write` | Create / update contacts.                      |

### Endpoints

| Method | Endpoint    | Required scope       |
| ------ | ----------- | -------------------- |
| `GET`  | `/contacts` | `fmg.contacts.read`  |
| `POST` | `/contacts` | `fmg.contacts.write` |

***

## Errors

| Situation                | Result                                                 |
| ------------------------ | ------------------------------------------------------ |
| User denies consent      | Redirect to `redirect_uri` with `error=access_denied`. |
| `state` mismatch         | Reject the callback — possible CSRF.                   |
| Invalid / expired code   | Token exchange returns `400`.                          |
| `code_verifier` mismatch | Token exchange returns `400`.                          |
| Expired access token     | API returns `401` — refresh the token (Step 6).        |
| Expired refresh token    | Refresh returns `400` — restart from Step 2.           |
| FMG unavailable          | `503` — transient; retry after a short delay.          |

***

## Security requirements

* **PKCE (S256)** for every request; keep the `code_verifier` server-side.
* **Unique `state`** per request; validate on callback.
* **Token exchange and refresh are server-side only** — never expose `client_secret` or `code_verifier` to the browser.
* **HTTPS** for all redirect URIs and API calls.
* Store tokens in HttpOnly cookies or server-side sessions.
