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

# Authentication

> How to authenticate with the LinkXG API

LinkXG supports two authentication methods: OAuth 2.0 for machine-to-machine integrations, and JWT tokens for user-context operations. This guide covers both.

## Authentication methods

| Method                           | Use case                                         | Token lifetime                        |
| -------------------------------- | ------------------------------------------------ | ------------------------------------- |
| **OAuth 2.0 Client Credentials** | Server-to-server integrations, automated systems | 15 minutes                            |
| **JWT Bearer Token**             | User-initiated actions, interactive applications | 15 minutes (access), 7 days (refresh) |

For most integrations, OAuth 2.0 is the recommended approach.

***

## OAuth 2.0 (recommended)

OAuth 2.0 Client Credentials flow is designed for machine-to-machine communication where no user interaction is required.

### Step 1: Create an OAuth client

OAuth clients are created in the LinkXG application by an Owner or Admin.

1. Go to **Account > API Access**
2. Select **Create OAuth Client**
3. Provide a name for the client (e.g., "ERP Integration")
4. Select the scopes you need
5. Save the client

You will receive a `client_id` and `client_secret`. Store the secret securely — it is shown only once.

### Step 2: Request an access token

Exchange your credentials for an access token:

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=products:read connections:read"
```

**Response:**

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "products:read connections:read"
}
```

The token expires in 900 seconds (15 minutes). Request a new token before it expires.

### Step 3: Use the token

Include the token in the `Authorization` header of all API requests:

```bash theme={null}
curl https://api.linkxg.com/api/v1/products \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

### Available scopes

| Scope               | Description                             |
| ------------------- | --------------------------------------- |
| `products:read`     | Read products and product versions      |
| `products:write`    | Create, update, and delete products     |
| `connections:read`  | Read connections and shared data        |
| `connections:write` | Manage connections                      |
| `imports:write`     | Bulk import operations                  |
| `traceability:read` | Read traceability and supply chain data |
| `dpp:read`          | Read Digital Product Passports          |

Request only the scopes you need. Tokens with broader scopes carry greater risk if compromised.

### Rotating client secrets

If your client secret is compromised, rotate it immediately:

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/oauth/clients/YOUR_CLIENT_ID/rotate \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

This invalidates the old secret and returns a new one. Update your integration before the old secret stops working.

***

## JWT authentication (user context)

JWT authentication is used when actions need to be performed in the context of a specific user, typically for interactive applications.

### Login

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@company.com",
    "password": "your_password"
  }'
```

**Response:**

```json theme={null}
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
    "user": {
      "id": "uuid",
      "email": "user@company.com",
      "role": "ADMIN"
    }
  }
}
```

### Using the token

Include the access token in requests:

```bash theme={null}
curl https://api.linkxg.com/api/v1/products \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

### Refreshing tokens

Access tokens expire after 15 minutes. Use the refresh token to obtain a new access token:

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "YOUR_REFRESH_TOKEN"
  }'
```

Refresh tokens are rotated on each use — you receive a new refresh token with each refresh request. The old refresh token is invalidated.

### Logout

Invalidate your session:

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/auth/logout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

## Cookie sessions (`/auth/v2`)

The LinkXG web application signs in through a separate, browser-first authentication surface under `/auth/v2`. Instead of returning tokens in the response body, these endpoints set the access, refresh, and CSRF tokens as `HttpOnly` cookies scoped to `.linkxg.com`. This is the path that powers passwordless sign-in (magic links) and single sign-on (SSO). It is intended for first-party browser clients; server-to-server integrations should continue to use OAuth 2.0.

All `/auth/v2` endpoints are public (no bearer token required) and enforce an `Origin`/`Referer` allowlist. On the JSON endpoints (`login`, `refresh`, `logout`, and the magic-link pair) a request that carries an `Origin` header which is not on the allowlist is rejected with `403`; the SSO redirect endpoints fail the flow rather than returning JSON. State-changing requests authenticated by the session cookie must also carry the `X-CSRF-Token` header matching the `linkxg_csrf` cookie.

### Cookies issued

| Cookie           | Purpose                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `linkxg_session` | Short-lived access token (`HttpOnly`)                                |
| `linkxg_refresh` | Refresh token, rotated on each use with reuse detection (`HttpOnly`) |
| `linkxg_csrf`    | Double-submit CSRF token, echoed back in the `X-CSRF-Token` header   |

`POST /auth/v2/login` (email + password), `POST /auth/v2/refresh` (rotates the session from the `linkxg_refresh` cookie; returns `204` with no body), and `POST /auth/v2/logout` (revokes the session and clears the cookies; idempotent, returns `204`) round out the session lifecycle.

### Magic-link sign-in

Magic links let a user sign in without a password. Request a link, then redeem the token it carries for a cookie session.

**Step 1 — request the link:**

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/auth/v2/magic-link/request \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@company.com" }'
```

To prevent account enumeration, this endpoint always returns `200` with a fixed message, whether or not the address maps to an account:

```json theme={null}
{ "success": true, "message": "If that address is registered, a sign-in link is on its way." }
```

The email contains a link whose single-use token is placed in the URL **fragment** (`#token=…`), not the query string, so it never reaches server logs. The client reads the token from `location.hash` and submits it in the request body — it must not be forwarded from the URL search parameters.

**Step 2 — redeem the token:**

```bash theme={null}
curl -X POST https://api.linkxg.com/api/v1/auth/v2/magic-link/redeem \
  -H "Content-Type: application/json" \
  -d '{ "token": "MAGIC_LINK_TOKEN" }'
```

Redeem accepts two kinds of token. An **existing-user sign-in token** sets the session cookies and returns the signed-in user — it materialises a session for a user whose company already exists and does not create a company:

```json theme={null}
{
  "success": true,
  "message": "Signed in.",
  "data": {
    "user": {
      "id": "uuid",
      "email": "user@company.com",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "role": "ADMIN",
      "tenantId": "uuid",
      "tenant": { "id": "uuid", "name": "Acme Ltd", "slug": "acme", "subscriptionTier": "SUPPLIER_FREE" }
    }
  }
}
```

A **passwordless sign-up intent token** provisions the tenant and user at redeem and then signs them in, returning the same envelope. If the sign-up instead resolves to a domain that gates joins, redeem creates an inactive user plus a join request and returns `200` with **no** cookies:

```json theme={null}
{ "success": true, "status": "pending_approval", "message": "Your request to join is awaiting approval." }
```

An invalid, expired, or already-used token returns `400 INVALID_OR_EXPIRED` with details withheld.

### Single sign-on (SSO)

SSO uses the OIDC Authorization Code flow with PKCE against **Microsoft Entra** (`microsoft`) and **Google** (`google`).

**Step 1 — start the flow.** Redirect the browser to the start endpoint. It generates a PKCE verifier and challenge, stores single-use OIDC state in Redis (10-minute TTL), and `302`-redirects to the identity provider. The optional `returnTo` query parameter (a path, validated server-side) controls where the user lands after sign-in.

```
GET https://api.linkxg.com/api/v1/auth/v2/sso/microsoft/start?returnTo=/manual
```

**Step 2 — handle the callback.** The provider redirects back to the callback endpoint with `code` and `state`. The server validates the state, exchanges the code for tokens, verifies the claims, and resolves the user:

* a returning SSO user or a new tenant founder is issued a v2 cookie session and redirected to `returnTo`;
* an existing same-email account is sent through magic-link confirmation;
* a domain join that needs owner/admin approval creates a join request and **no** session is issued.

```
GET https://api.linkxg.com/api/v1/auth/v2/sso/microsoft/callback?code=…&state=…
```

Both SSO endpoints respond with redirects (`302`) rather than JSON, so they are driven by the browser, not called directly by an integration.

***

## Token handling best practices

**Store tokens securely.** Never store tokens in client-side code, localStorage, or version control. Use secure server-side storage or environment variables.

**Refresh proactively.** Request a new token before the current one expires. A token that expires mid-request will fail.

**Handle 401 errors gracefully.** If you receive a 401 Unauthorized response, your token may have expired. Refresh it and retry the request.

**Use the minimum necessary scope.** Tokens with broader scopes carry greater risk. Request only what you need.

**Rotate secrets regularly.** Even without a suspected compromise, rotating client secrets periodically reduces risk.

***

## Error responses

Authentication errors return standard HTTP status codes:

| Status | Error                 | Description                                        |
| ------ | --------------------- | -------------------------------------------------- |
| 400    | `invalid_request`     | Malformed request (missing fields, invalid format) |
| 401    | `invalid_client`      | Client credentials are incorrect                   |
| 401    | `invalid_token`       | Token is expired, malformed, or revoked            |
| 403    | `insufficient_scope`  | Token does not have the required scope             |
| 429    | `rate_limit_exceeded` | Too many authentication requests                   |

**Example error response:**

```json theme={null}
{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}
```

***

## Next steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference">
    Browse the complete API documentation
  </Card>

  <Card title="Rate limits" icon="gauge" href="/developers/rate-limits">
    Understand usage limits and quotas
  </Card>
</CardGroup>
