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

# Authentication and access scope

> Authenticate requests with X-API-Key, select tenant context, and handle credential failures.

<Badge color="green" shape="pill" icon="shield-check">Server-to-server</Badge> <Badge color="blue" shape="pill" icon="key-round">API key</Badge>

All protected JobHandy operations require an API key in the `X-API-Key` header. `GET /health` is the only public operation.

<Info>
  Need a credential? Follow [API Key Management](/get-started/api-key-management) before continuing.
</Info>

## Send the API key

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request GET \
    --url 'https://api.jobhandy.io/v1/employees' \
    --header 'X-API-Key: YOUR_API_KEY'
  ```

  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch('https://api.jobhandy.io/v1/employees', {
    headers: {
      'X-API-Key': process.env.JOBHANDY_API_KEY,
    },
  });

  if (!response.ok) {
    throw new Error(`JobHandy request failed: ${response.status}`);
  }
  ```

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

  response = requests.get(
      'https://api.jobhandy.io/v1/employees',
      headers={'X-API-Key': os.environ['JOBHANDY_API_KEY']},
      timeout=30,
  )
  response.raise_for_status()
  ```

  ```csharp C# theme={"theme":{"light":"github-light","dark":"github-dark"}}
  using var client = new HttpClient
  {
      BaseAddress = new Uri("https://api.jobhandy.io/v1/")
  };
  client.DefaultRequestHeaders.Add(
      "X-API-Key",
      Environment.GetEnvironmentVariable("JOBHANDY_API_KEY")
  );
  using var response = await client.GetAsync("employees");
  response.EnsureSuccessStatusCode();
  ```

  ```powershell PowerShell theme={"theme":{"light":"github-light","dark":"github-dark"}}
  $headers = @{
      'X-API-Key' = $env:JOBHANDY_API_KEY
  }
  Invoke-RestMethod `
      -Method Get `
      -Uri 'https://api.jobhandy.io/v1/employees' `
      -Headers $headers
  ```
</CodeGroup>

## Select a tenant

An API key may include one or more tenants. Where documented, `X-Tenant-ID` selects one tenant that is already inside the key's scope.

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
X-Tenant-ID: {{tenantId}}
```

| Situation                                     | Header behavior                                              |
| --------------------------------------------- | ------------------------------------------------------------ |
| Collection read across all authorized tenants | Omit `X-Tenant-ID`                                           |
| Collection read for one authorized tenant     | Send `X-Tenant-ID`                                           |
| Write with one unambiguous target tenant      | The API may infer the tenant as documented by that operation |
| Write with multiple possible target tenants   | Send `X-Tenant-ID` or another documented placement selector  |
| Tenant outside API-key scope                  | Request fails with `TENANT_NOT_IN_SCOPE`                     |

<Note>
  `X-Tenant-ID` never grants access. It only narrows the scope already assigned to the key.
</Note>

## Authentication flow

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Client
    participant API as JobHandy API
    Client->>API: Request + X-API-Key
    API->>API: Validate key and active status
    API->>API: Load assigned tenant and division scope
    opt X-Tenant-ID supplied
        API->>API: Verify selected tenant is in scope
    end
    alt Authorized
        API-->>Client: Response + X-Request-ID
    else Missing, invalid, or inactive key
        API-->>Client: 401 INVALID_API_KEY
    else Tenant outside scope
        API-->>Client: 403 TENANT_NOT_IN_SCOPE
    end
```

## Credential failures

|  HTTP | Error code            | Meaning                                       | Corrective action                | Retry |
| ----: | --------------------- | --------------------------------------------- | -------------------------------- | ----: |
| `401` | `INVALID_API_KEY`     | Key is missing, invalid, deleted, or inactive | Verify the secret and key status |    No |
| `403` | `TENANT_NOT_IN_SCOPE` | Selected tenant is outside the key scope      | Correct the tenant or key scope  |    No |
| `400` | `UNSUPPORTED_HEADER`  | A request header is not accepted              | Remove the unsupported header    |    No |
| `400` | `INVALID_REQUEST_ID`  | `X-Request-ID` is not a UUID v4 or v7         | Generate a valid UUID            |    No |

## Credential handling rules

<AccordionGroup>
  <Accordion title="Secret storage" defaultOpen icon="lock-keyhole">
    Store the key in a managed secret store or protected environment variable. Restrict read access to the integration runtime.
  </Accordion>

  <Accordion title="Logging">
    Never log the complete key. Log the returned `X-Request-ID`, operation, status, error code, and a non-secret internal credential label.
  </Accordion>

  <Accordion title="Rotation">
    Create and verify a replacement key before deactivating the old credential. See [Rotate API keys](/guides/rotate-api-keys).
  </Accordion>

  <Accordion title="Client boundary">
    Do not expose the key to browser code, mobile applications, shared workstations, or customer-controlled scripts.
  </Accordion>
</AccordionGroup>
