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

# Authentication

> Organization API keys and OAuth 2.1 for API access.

The Grain API supports two authentication methods:

* **Organization API keys** for server automation owned by one church. This is the fastest way to test the production API.
* **OAuth 2.1 (Sign in with Grain)** for applications that act on behalf of individual users, especially apps serving multiple organizations.

<Info>
  REST API and MCP access require an active Enterprise plan for the organization you are accessing.
</Info>

<Warning>
  Store API keys, access tokens, and refresh tokens in a secrets manager. Never expose credentials in client-side code, logs, or public repositories.
</Warning>

## Organization API keys

An Owner, Admin, or custom role with **Manage API access** can create a key in **Settings → API**. Give the key a descriptive name, choose the smallest required scope set, and select an expiry. The secret is displayed only once.

Production organizations receive keys beginning with `grain_live_`. Sandbox, demo, and test organizations receive `grain_test_` keys.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl "https://api.grainledger.com/api/v1/transactions?page=1&limit=20" \
  -H "Authorization: Bearer grain_live_YOUR_KEY"
```

The key determines its organization, so `X-Organization-Id` is optional. If you include the header, it must match the key's organization. API keys work only under `/api/v1`; they cannot sign in to the Grain app, call tRPC or MCP, or authenticate directly with the identity provider.

### API key scopes

| Resource            | Read                 | Write                 |
| ------------------- | -------------------- | --------------------- |
| Transactions        | `transactions:read`  | `transactions:write`  |
| Bills               | `bills:read`         | `bills:write`         |
| Donors              | `donors:read`        | `donors:write`        |
| Contributions       | `contributions:read` | `contributions:write` |
| Pledges             | `pledges:read`       | `pledges:write`       |
| Households          | `households:read`    | `households:write`    |
| Pledge card imports | —                    | `pledge-cards:write`  |

Write access includes the corresponding read access. Donor and household scopes can expose personal information. Contribution and pledge responses include donor IDs but never embed donor identity; use a donor read scope to retrieve the related donor separately.

### Rotation and revocation

Rotating a key displays a new secret once. The previous secret remains valid for up to 24 hours, or until its original expiry if sooner, so you can update automation without downtime. You can revoke the previous secret immediately after deployment. Full revocation permanently disables both secrets; the named key stays visible for audit history.

## OAuth 2.1 flow

<Steps>
  <Step title="Redirect to authorize">
    Send the user to `https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/authorize` with your client ID, redirect URI, scopes, and PKCE parameters (`code_challenge` and `code_challenge_method=S256`).
  </Step>

  <Step title="User consents">
    The user signs in (if needed) and approves access at `https://app.grainledger.com/oauth/consent`.
  </Step>

  <Step title="Receive authorization code">
    Grain redirects back to your `redirect_uri` with an authorization `code` and `state` parameter.
  </Step>

  <Step title="Exchange for tokens">
    POST the code to `https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/token` with your `code_verifier` to receive access and refresh tokens.
  </Step>

  <Step title="Call the API">
    Include `Authorization: Bearer <access_token>` and `X-Organization-Id` on every API request.
  </Step>
</Steps>

## OAuth endpoints

| Purpose         | URL                                                                                       |
| --------------- | ----------------------------------------------------------------------------------------- |
| Authorization   | `https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/authorize`                        |
| Token           | `https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/token`                            |
| Consent UI      | `https://app.grainledger.com/oauth/consent`                                               |
| OAuth discovery | `https://pqnoojlycpbwbatbdgep.supabase.co/.well-known/oauth-authorization-server/auth/v1` |
| OIDC discovery  | `https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/.well-known/openid-configuration`       |
| MCP PRM         | `https://api.grainledger.com/.well-known/oauth-protected-resource`                        |

## MCP (AI agents)

Grain exposes a read-only MCP server at `https://api.grainledger.com/mcp` for ChatGPT and other MCP clients. MCP uses the same OAuth identity and organization permissions; org scoping is via `organizationId` in tool arguments instead of `X-Organization-Id`. See [MCP](/mcp-server).

MCP is read-only because it exposes only read tools. OAuth tokens still identify the signed-in user, and Grain checks organization permissions on each tool call.

## Authorization request example

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/oauth/callback
  &scope=openid%20email%20profile
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &state=af0ifjsldkj
```

<Tip>
  Generate `code_challenge` as the Base64URL-encoded SHA-256 hash of a random `code_verifier` string. Keep the verifier secret until the token exchange.
</Tip>

## Token exchange

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code" \
    -d "code=AUTHORIZATION_CODE" \
    -d "redirect_uri=https://yourapp.com/oauth/callback" \
    -d "client_id=YOUR_CLIENT_ID" \
    -d "code_verifier=YOUR_CODE_VERIFIER"
  ```

  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const tokenResponse = await fetch("https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: redirectUri,
      client_id: clientId,
      code_verifier: codeVerifier,
    }),
  });
  ```
</CodeGroup>

## X-Organization-Id header with OAuth

Every OAuth API request must include the organization (church) whose data you are accessing:

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
X-Organization-Id: cm123abc456def
```

Users can belong to multiple organizations. The OAuth flow does not scope to a single org; your application specifies the org per request. See [Organizations](/organizations) for how to obtain the ID.

## OAuth scopes

Standard OIDC scopes control user profile data in tokens:

| Scope     | Description                  |
| --------- | ---------------------------- |
| `openid`  | OpenID Connect identity      |
| `email`   | Email address                |
| `profile` | Name and profile information |
| `phone`   | Phone number                 |

API access is granted per user session. Permissions (for example view transactions, manage donors) are enforced based on the user's role in the specified organization.

## Example API request

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X GET "https://api.grainledger.com/api/v1/donors?page=1&limit=20" \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
    -H "X-Organization-Id: cm123abc456def" \
    -H "Content-Type: application/json"
  ```

  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch(
    "https://api.grainledger.com/api/v1/donors?page=1&limit=20",
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "X-Organization-Id": organizationId,
        "Content-Type": "application/json",
      },
    }
  );
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  response = requests.get(
      "https://api.grainledger.com/api/v1/donors",
      params={"page": 1, "limit": 20},
      headers={
          "Authorization": f"Bearer {access_token}",
          "X-Organization-Id": organization_id,
      },
  )
  ```
</CodeGroup>

## Token refresh

Use the refresh token from the token exchange to obtain new access tokens when they expire:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://pqnoojlycpbwbatbdgep.supabase.co/auth/v1/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
```

Refresh tokens before they expire to avoid interrupting active integrations.
