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

# Connecting your app over OAuth 2.1

> This guide is for third-party developers building an app, integration, or AI tool (e.g. an MCP client) that needs to act on a Flow9 workspace's behalf.

This guide is for **third-party developers** building an app, integration, or AI
tool (e.g. an MCP client) that needs to act on a Flow9 workspace's behalf.

OAuth lets a Flow9 user **grant your app scoped, revocable access without ever
sharing a password or a long-lived API key**. The user stays in control: they
choose what your app may do on a consent screen, and they can revoke it at any
time from **Settings → Connected Apps**.

> If you just need a single server-to-server credential for your *own*
> workspace, an [API key](/api-reference/introduction) is simpler. Use OAuth when **another**
> Flow9 user needs to authorize your app against **their** workspace.

***

## The endpoints

Everything is discoverable from the authorization-server metadata:

```
GET https://mwxpyoqrtdfxubdotbmj.supabase.co/auth/v1/.well-known/oauth-authorization-server
```

That document lists the canonical URLs. The ones you'll use:

| Purpose                       | Endpoint                               |
| ----------------------------- | -------------------------------------- |
| Register your app             | `POST /auth/v1/oauth/clients/register` |
| Send the user to authorize    | `GET /auth/v1/oauth/authorize`         |
| Exchange the code for a token | `POST /auth/v1/oauth/token`            |
| Public keys (verify tokens)   | `GET /auth/v1/.well-known/jwks.json`   |
| Call the Flow9 API            | `…/functions/v1/public-api/v1/*`       |

All requests carry the project's public `apikey` header (the anon key), exactly
as the [REST guide](/api-reference/introduction) describes.

***

## Step 1 — Register your app (once)

Register with Dynamic Client Registration to get a `client_id`. Public clients
(desktop / CLI / native apps that can't keep a secret) use PKCE and get no secret.

```bash theme={null}
curl -X POST 'https://mwxpyoqrtdfxubdotbmj.supabase.co/auth/v1/oauth/clients/register' \
  -H 'apikey: <ANON_KEY>' \
  -H 'content-type: application/json' \
  -d '{
    "client_name": "Acme Integrator",
    "redirect_uris": ["https://your-app.example.com/callback"],
    "grant_types": ["authorization_code"],
    "token_endpoint_auth_method": "none"
  }'
```

Store the returned `client_id`. Your `redirect_uris` must exactly match what you
send in Step 2.

***

## Step 2 — Send the user to authorize (with PKCE)

Generate a PKCE `code_verifier` (random) and its `code_challenge`
(`base64url(sha256(verifier))`), then open the authorize URL in the user's
browser:

```
https://mwxpyoqrtdfxubdotbmj.supabase.co/auth/v1/oauth/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=https://your-app.example.com/callback
  &scope=openid profile email
  &state=<RANDOM>
  &code_challenge=<CHALLENGE>
  &code_challenge_method=S256
```

Notes:

* Request the **standard OIDC scopes** (`openid profile email`). You do **not**
  request Flow9 data permissions here — those are chosen by the workspace owner
  on the consent screen (Step 3).
* Add `&prompt=consent` if you want to force the consent screen even for a
  returning user (otherwise a user who already approved is sent straight through).

***

## Step 3 — The user consents

Flow9 shows the user a consent screen listing your app and four **permission
groups**. The user ticks what your app may do:

| Group          | Grants                                                             | Default |
| -------------- | ------------------------------------------------------------------ | ------- |
| **Records**    | Read/create/edit leads, customers, activities                      | on      |
| **Automation** | Read/trigger workflows, manage webhook subscriptions               | on      |
| **Messaging**  | Send SMS/email, place calls — *consumes the plan's paid allowance* | off     |
| **Admin**      | Manage workspace settings and credentials                          | off     |

Spend-capable (Messaging) and sensitive (Admin) groups are **off by default**, so
a careless "Allow" never hands out paid or administrative actions. Your app
receives whatever the user approves — request only what you need and degrade
gracefully if a scope wasn't granted (you'll get `403 FORBIDDEN_SCOPE`).

On approval the browser is redirected to your `redirect_uri` with `?code=…&state=…`.
Verify `state` matches what you sent.

***

## Step 4 — Exchange the code for a token

```bash theme={null}
curl -X POST 'https://mwxpyoqrtdfxubdotbmj.supabase.co/auth/v1/oauth/token' \
  -H 'apikey: <ANON_KEY>' \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=<CODE>' \
  --data-urlencode 'redirect_uri=https://your-app.example.com/callback' \
  --data-urlencode 'client_id=<CLIENT_ID>' \
  --data-urlencode 'code_verifier=<VERIFIER>'
```

You get back an `access_token` (a signed ES256 JWT) and a `refresh_token`. The
access token is short-lived; use the refresh token to get a new one when it
expires.

***

## Step 5 — Call the Flow9 API

Send the access token as a Bearer credential (see [auth.md](/api-reference/authentication)):

```bash theme={null}
curl 'https://mwxpyoqrtdfxubdotbmj.supabase.co/functions/v1/public-api/v1/customers?email=sarah@example.com' \
  -H 'apikey: <ANON_KEY>' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'
```

An OAuth token behaves **exactly like an API key with the same scopes** — the same
routes, the same [errors](/api-reference/errors), the same [rate limits](/api-reference/rate-limits).
A request with a valid token but no matching grant gets `403 FORBIDDEN_SCOPE`
listing what was required.

***

## Revocation

The workspace owner can revoke your app at any time from **Settings → Connected
Apps**. Revocation is immediate: existing tokens stop authorizing straight away,
and the next call returns `403`. If the user reconnects later, they'll be asked to
consent again — so re-request access through Step 2 rather than reusing an old
token.

***

## Errors you should handle

| Situation                              | Response                                          |
| -------------------------------------- | ------------------------------------------------- |
| No credential                          | `401` with `WWW-Authenticate: Bearer`             |
| Expired / invalid / wrong-issuer token | `401` `AUTH_INVALID_KEY`                          |
| Valid token, action not granted        | `403` `FORBIDDEN_SCOPE` (with the required scope) |
| Access revoked by the user             | `403` — start over at Step 2                      |

Treat `401` as "refresh or re-authorize" and `403` as "you weren't granted this —
ask the user for that permission group."
