> Source: https://wexa.ai/docs/sdks-and-apis/login

# login

Log a Wexa user in with the email and password they use for the console and get an **SDK session** for one project. The session acts as that user, with their current role, so every call is checked and audited as them. It returns an access token (one hour) and a refresh token (30 days, new on every use). Not on the MCP server: its clients sign in through OAuth.

**Log a user in**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/auth/login \
  -H "content-type: application/json" \
  -d '{"email": "ada@example.com", "password": "'"$FABRIC_PASSWORD"'", "project_id": "6aa9beb7ec8121a43b6c791b"}'
```

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "refresh_token": "fab_srt_Ja2bvA…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "project_id": "6aa9beb7ec8121a43b6c791b",
  "session_id": "sess_Ss5LcvniPbkIkWERMiY8YUAO"
}
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

// Resolves to a ready client; onTokens receives { accessToken, refreshToken, expiresAt }
const fabric = await Fabric.login({
  workspace: 'https://fabric.wexa.ai',
  email: 'ada@example.com',
  password,
  projectId: '6aa9beb7ec8121a43b6c791b',
  onTokens: (tokens) => saveTokens(tokens),
})
const result = await fabric.whoami()
```

**Python SDK**

```python
from wexa import Fabric

# Returns a ready client; on_tokens receives {"access_token", "refresh_token", "expires_at"}
fabric = Fabric.login(
    "https://fabric.wexa.ai", "ada@example.com", password,
    "6aa9beb7ec8121a43b6c791b", on_tokens=save_tokens,
)
me = fabric.whoami()
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `email` | string | Required | The user's Wexa email address. |
| `password` | string | Required | The user's Wexa password. A wrong email or password is refused (401, `invalid_credentials`); the two cases look the same on purpose. |
| `project_id` | string | Required | The project the session is locked to. `projectId` in the TypeScript SDK. Must be a project id, or the call is refused (400, `invalid_request`). The user must be a member of it, or the call is refused (403, `forbidden`). |
| `grants` | string[] | Optional | Narrows the session to fewer grants than the user's role gives, for example `["fabric:query.read"]`. Every grant named must be in the user's role, or the call is refused (400, `invalid_request`). |
| `workspace` | string | SDK only | Your Wexa deployment's URL. |
| `onTokens` / `on_tokens` | function | SDK only | Optional callback receiving every new token pair, starting with this one. |

**Errors**

- `ValidationError` (400, `invalid_request`): the body is not JSON, or `email`, `password` or `project_id` is missing or empty.
- `ForbiddenError` (403): the user is suspended in its organization (`account_suspended`), or the call came from a browser origin that is not allowed (`origin_not_allowed`).
- `QuotaExceeded` (429): too many failed logins for this email or address. `retry_after` / `retryAfter` says how many seconds to wait.
- `UpstreamError` (502): the account service behind the gateway could not be reached. Retrying later can help.

**Show full response**

A real response, captured from a live call to `login`. The tokens are cut short here:

**Result of login**

**REST API**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "refresh_token": "fab_srt_Ja2bvA…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "org_id": "6a79a770b6c128ccc24bfca8",
  "project_id": "6aa9beb7ec8121a43b6c791b",
  "session_id": "sess_Ss5LcvniPbkIkWERMiY8YUAO",
  "grants": [
    "fabric:query.read",
    "fabric:docs.read",
    "fabric:ontology.write",
    "fabric:orchestrate.read",
    "fabric:orchestrate.write",
    "fabric:agent.run",
    "fabric:skill.write",
    "fabric:model.write",
    "fabric:model.run",
    "fabric:connector.run",
    "fabric:flow.delete",
    "fabric:kb.delete",
    "fabric:table.read",
    "fabric:table.write"
  ]
}
```

**TypeScript SDK**

```ts
// Fabric.login resolves to a ready client. onTokens receives the pair:
{
  accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…',
  refreshToken: 'fab_srt_Ja2bvA…',
  expiresAt: 1790253600
}
```

**Python SDK**

```python
# Fabric.login returns a ready client. on_tokens receives the pair:
{
 "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
 "refresh_token": "fab_srt_Ja2bvA…",
 "expires_at": 1790253600.0
}
```

`expires_at` is the access token's expiry in seconds since the epoch. `whoami` on the new client reports `auth_kind: "session"` and the `session_id`.

**More details**

Use an [API key](/docs/get-started/authentication#create-an-api-key) when a service calls Wexa as itself; use `login` when you build your own login page on top of Wexa. Both SDKs keep the token pair in memory, refresh the access token before it runs out, and hand you every new pair so you can save it. See [refresh-session](/docs/sdks-and-apis/refresh-session) and [logout](/docs/sdks-and-apis/logout).

In a browser, login and every later call work only from origins your project admin lists under **Developer access → Allowed origins** in the console. Calls from a server are not affected.

An unverified email can log in for seven days after signup, the same as in the console; after that, login is refused until the email is verified.

`ConfigurationError` is raised in the SDK before any request when a required option is missing, or when an API key and a token pair were both given.

Not a governed tool: it runs before any tool call. Every call the new session makes afterwards runs through the full lifecycle as the logged-in user.

### Resume a saved session

Build the client from the saved pair instead of logging in again. It refreshes on its own when the access token runs out. Over REST there is no client: send the saved access token as the bearer, and when a call comes back `401` with `invalid_token`, trade the refresh token for a new pair and save it.

**Resume a session**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/whoami \
  -H "authorization: Bearer $FABRIC_ACCESS_TOKEN"

# after a 401 invalid_token: get a new pair, then save both tokens
curl -sS https://fabric.wexa.ai/v1/auth/refresh \
  -H "content-type: application/json" \
  -d '{"refresh_token": "'"$FABRIC_REFRESH_TOKEN"'"}'
```

**TypeScript SDK**

```ts
const fabric = new Fabric({
  workspace: 'https://fabric.wexa.ai',
  accessToken: saved.accessToken,
  refreshToken: saved.refreshToken,
  onTokens: (tokens) => saveTokens(tokens),
})
```

**Python SDK**

```python
fabric = Fabric(
    "https://fabric.wexa.ai",
    access_token=saved["access_token"],
    refresh_token=saved["refresh_token"],
    on_tokens=save_tokens,
)
```

### Ask for less than the role gives

**Narrow the session**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/auth/login \
  -H "content-type: application/json" \
  -d '{"email": "ada@example.com", "password": "'"$FABRIC_PASSWORD"'", "project_id": "6aa9beb7ec8121a43b6c791b", "grants": ["fabric:query.read"]}'
```

**TypeScript SDK**

```ts
const reader = await Fabric.login({
  workspace, email, password, projectId,
  grants: ['fabric:query.read'],
})
```

**Python SDK**

```python
reader = Fabric.login(workspace, email, password, project_id,
                      grants=["fabric:query.read"])
```

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
