# GraphQL Authentication

The Fynex GraphQL API lives at **`/dashboard/graphql`**. Unlike the REST API — which uses a bearer token — this endpoint is authenticated exclusively with an **HttpOnly session cookie** named `dashboard_session`. There is no bearer-token path to `/dashboard/graphql`.

This endpoint is the right choice when you need operations that have no REST equivalent: full payee CRUD, payout method management, wallet queries, split rules, reconciliation data, and token rotation.

---

## When to use the GraphQL endpoint

Use `/dashboard/graphql` (cookie-session auth) when you need to:

- Create, update, or delete payees and their payout methods.
- Query wallets, reconciliation statements, or split executions.
- Rotate the bearer token that your server-to-server REST integration uses (`regenerateSellerAccountToken`).
- Access any query or mutation not exposed on the REST surface.

> [!NOTE]
> Sellers who build **custom dashboard integrations** — or who need programmatic access to payee/wallet data — are the primary audience for this guide.

---

## Authentication flow

1. **Obtain a session cookie** by posting credentials to the login endpoint:

   ```http
   POST /api/v1/login/dashboard
   Content-Type: application/json

   { "email": "you@example.com", "password": "your_password" }
   ```

   On success (`200 OK`) the server sets an HttpOnly cookie named `dashboard_session`. The response body also returns the user ID, permissions, and onboarding status — you can discard those fields for a pure API integration.

2. **Send GraphQL requests** to `/dashboard/graphql` with the cookie attached. Set `Content-Type: application/json` and include the cookie on every request.

3. **Log out** when done (optional but recommended for server-side scripts):

   ```http
   POST /api/v1/logout
   ```

   This clears the cookie server-side.

> [!CAUTION]
> **A new account is a draft until `complete`.** `POST /api/v1/onboarding/start` creates the user and **does** set the `dashboard_session` cookie, but the account has no organization yet — GraphQL calls that need one fail until `POST /api/v1/onboarding/complete` has run (see [Account setup & onboarding](https://api.fynex.ai/payments-api/v2/docs#tag/onboarding)). For an existing account, `POST /api/v1/login/dashboard` is the way to a cookie.

---

## Code samples — login and first query

#### curl

```bash
# Step 1 — login and save the cookie
curl -sc cookies.txt \
  -X POST https://api.fynex.ai/api/v1/login/dashboard \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your_password"}'

# Step 2 — send a GraphQL query using the saved cookie
curl -b cookies.txt \
  -X POST https://api.fynex.ai/dashboard/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query ListPayees($limit: Int, $offset: Int) { payees(limit: $limit, offset: $offset) { id displayName role status } }",
    "variables": { "limit": 20, "offset": 0 }
  }'
```

#### JavaScript

```js
const BASE = 'https://api.fynex.ai';

// Step 1 — login; browser (or same-origin server) sends/receives the cookie automatically
async function login(email, password) {
  const res = await fetch(`${BASE}/api/v1/login/dashboard`, {
    method: 'POST',
    credentials: 'include', // required — sends and stores the HttpOnly cookie
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error(`Login failed: ${res.status}`);
  return res.json(); // { userId, permissions, onboardingStatus }
}

// Step 2 — send any GraphQL operation
async function gql(query, variables = {}) {
  const res = await fetch(`${BASE}/dashboard/graphql`, {
    method: 'POST',
    credentials: 'include', // cookie is attached automatically
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  const { data, errors } = await res.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data;
}

// Usage
await login('you@example.com', 'your_password');
const { payees } = await gql(
  `query ($limit: Int, $offset: Int) {
     payees(limit: $limit, offset: $offset) { id displayName role status }
   }`,
  { limit: 20, offset: 0 }
);
console.log(payees);
```

#### Python

```python
import requests

BASE = "https://api.fynex.ai"

# requests.Session persists cookies across calls automatically
session = requests.Session()

# Step 1 — login
resp = session.post(
    f"{BASE}/api/v1/login/dashboard",
    json={"email": "you@example.com", "password": "your_password"},
)
resp.raise_for_status()  # raises on 4xx/5xx

# Step 2 — send a GraphQL query
query = """
  query ListPayees($limit: Int, $offset: Int) {
    payees(limit: $limit, offset: $offset) {
      id
      displayName
      role
      status
    }
  }
"""
resp = session.post(
    f"{BASE}/dashboard/graphql",
    json={"query": query, "variables": {"limit": 20, "offset": 0}},
)
resp.raise_for_status()
data = resp.json()
if "errors" in data:
    raise RuntimeError(data["errors"][0]["message"])
print(data["data"]["payees"])
```

---

## Session properties

| Property | Value |
|----------|-------|
| Cookie name | `dashboard_session` |
| Cookie flags | HttpOnly, SameSite (not accessible from JavaScript) |
| Session TTL | 24 hours (in-memory; not clustered — a server restart invalidates all sessions) |
| Auth required | Every request to `/dashboard/graphql` must carry the cookie |
| Multiple sessions | Each login creates a new session; logout clears both `dashboard_session` and `backoffice_session` |

---

## Receipts are bound to the session

Some mutations refuse to act on a bare id and require a **receipt** issued by
an earlier call — `activateSplitRule` takes the `activationReceipt` that
`previewSplit(ruleId:)` returned. That receipt is signed with a key derived
from the `dashboard_session` that ran the preview, and it also names the
actor, the rule and a fingerprint of the exact rule snapshot previewed. It is
valid for **10 minutes**.

The consequence for a scripted integration: preview and activate must happen
**inside the same session**. A receipt presented from a different cookie —
after a re-login, from a second worker, or after a server restart invalidated
the session — is refused with *"invalid split activation preview receipt"*,
and an expired one with *"split activation preview receipt expired; run
preview again"*. Editing the rule in between invalidates it too. Hold one
session for the whole preview → activate sequence, and treat any receipt error
as "preview again", never as "retry the activation".

See **[Split payments](https://api.fynex.ai/payments-api/v2/docs#tag/splits)** for the full lifecycle.

---

## GraphQL endpoint details

| Property | Value |
|----------|-------|
| URL | `https://api.fynex.ai/dashboard/graphql` |
| Method | `POST` |
| Content-Type | `application/json` |
| Body shape | `{ "query": "...", "variables": { ... } }` |
| Error shape | `{ "errors": [{ "message": "..." }] }` |

A GraphQL Playground is available at `GET /dashboard/playground` (no auth gate on the playground page itself — useful for manual exploration).

---

## Rotating your bearer token from GraphQL

`regenerateSellerAccountToken` is the one mutation that bridges the GraphQL session world back to the REST bearer-token world. Call it to rotate the `authorizationToken` used by your server-to-server integration.

```graphql
mutation RotateToken($merchantId: ID!) {
  regenerateSellerAccountToken(merchantId: $merchantId) {
    id
    authorizationToken
  }
}
```

Supply your **seller account ID** as `$merchantId` — the argument is a GraphQL `ID`, so `"42"` and `42` are both accepted. The response contains the new bearer token. The previous token is invalidated immediately — update your secret manager before calling this.

> [!CAUTION]
> There is no overlap window between the old and new token. Plan a brief service restart or atomic secret rotation before calling this mutation in production.

---

## Common pitfalls

| Pitfall | Resolution |
|---------|------------|
| Calling `/dashboard/graphql` with a Bearer token | This endpoint does not support bearer auth. Use the `dashboard_session` cookie instead. |
| GraphQL fails right after sign-up | A draft account (after `POST /api/v1/onboarding/start`) has no organization yet. Finish the wizard and call `POST /api/v1/onboarding/complete` first. |
| Session lost on server restart | Sessions are held in-memory on a single node. A restart invalidates all active sessions — clients must re-authenticate. |
| Cookie not sent by the browser | Ensure you use `credentials: 'include'` on every `fetch` call (or the equivalent in your HTTP client). |
| `401` on the GraphQL endpoint | Either the cookie is absent, expired (>24 h), or was invalidated by a server restart. Re-login to get a fresh cookie. |
| `activateSplitRule` rejects a receipt that `previewSplit` just issued | The two calls ran under different sessions (a re-login or a second worker in between), or more than 10 minutes passed. Receipts are session-bound — see **Receipts are bound to the session** above. Run the preview again in the session that will activate. |

## See also

- **[Authentication & Tokens (Bearer)](https://api.fynex.ai/payments-api/v2/docs#tag/authentication)** — Bearer token auth for the server-to-server REST API.
- **[Payees](https://api.fynex.ai/payments-api/v2/docs#tag/payees)** — Create and manage payees via REST and GraphQL.
- **[Payout Methods](https://api.fynex.ai/payments-api/v2/docs#tag/payout-methods)** — Register bank accounts as payout destinations for your payees.
