---
id: public-api.authentication
title: API Authentication
summary: Distinguish API-key capability, delegated External Agents login, current destination access, and browser authentication.
kind: api
product_area: public_api
status: stable
updated: 2026-10-31
canonical: /docs/public-api/authentication
---

# API Authentication

## Definition

The gateway accepts API keys and delegated native-login access tokens on their supported routes. Browser session cookies remain the interactive app path; these credential types are not interchangeable.

## API keys

Keys belong to a workspace. Base capability can be read-only, full access or Fieldloop submission. Project restrictions, allowed actions and expiry narrow that capability when supplied.

A new Fieldloop submission key carries that workspace capability. The caller still supplies the Project and source destination, and current access is checked at intake. Previously issued restricted keys retain their restrictions.

Changing access requires issuing the intended credential; renaming a key does not broaden it. Never place values in URLs, source, logs or copied agent tasks.

## Delegated login

Native login is scoped to supported External Agents benchmark routes and organization login status. Delegation uses `external-agents:read` and `external-agents:write`, plus current project read/manage authorization. It does not provision a user, select a fallback organization or grant every endpoint.

The native client uses authorization-code login with PKCE and an OS credential store. Auth discovery/session routes are distinct from customer operations.

## Request headers

Supply the credential as a bearer token. `TEAMMATELY_API_KEY` is injected by your secret mechanism; configure `PROJECT_URL` with the [quickstart](/docs/public-api/quickstart).

### Example: Read a Case page

```sh
curl --fail-with-body \
  --header "Authorization: Bearer ${TEAMMATELY_API_KEY}" \
  "${PROJECT_URL}/cases?limit=1&offset=0"
```

```python
import os
from urllib.parse import quote, urlencode
from urllib.request import HTTPRedirectHandler, Request, build_opener

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

opener = build_opener(NoRedirect())
api_key = os.environ["TEAMMATELY_API_KEY"]

url = os.environ["PROJECT_URL"] + "/cases"
url += "?" + urlencode({"limit": "1", "offset": "0"})
headers = {"Authorization": "Bearer " + api_key}
request = Request(url, headers=headers, method="GET")
with opener.open(request) as response:
    print(response.read().decode("utf-8"))
```

```typescript
const apiKey = process.env.TEAMMATELY_API_KEY!;

{
  const url = new URL(process.env.PROJECT_URL! + "/cases");
  url.searchParams.set("limit", "1");
  url.searchParams.set("offset", "0");
  const response = await fetch(url, {
    method: "GET",
    redirect: "error",
    headers: {
      Authorization: `Bearer ${apiKey}`,
    },
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  console.log(await response.text());
}
```

```go
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
)

func main() {
    if err := run(context.Background()); err != nil { log.Fatal(err) }
}

func run(ctx context.Context) error {
    client := &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    {
        target := os.Getenv("PROJECT_URL") + "/cases"
        query := url.Values{}
        query.Set("limit", "1")
        query.Set("offset", "0")
        target += "?" + query.Encode()
        request, err := http.NewRequestWithContext(ctx, "GET", target, nil)
        if err != nil { return err }
        request.Header.Set("Authorization", "Bearer " + os.Getenv("TEAMMATELY_API_KEY"))
        response, err := client.Do(request)
        if err != nil { return err }
        payloadBytes, readErr := io.ReadAll(response.Body)
        response.Body.Close()
        if readErr != nil { return readErr }
        fmt.Println(string(payloadBytes))
        if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("HTTP %d", response.StatusCode) }
    }
    return nil
}
```

## Choose the credential

| Operation | Credential boundary |
| --- | --- |
| Case reads | API key with Project read access |
| Case imports/reference writes | API key with the required write access |
| Fieldloop submission/receipts | Fieldloop capability, destination permission and original-submitter receipt ownership |
| External Agents reads | Authorized API key, or delegated `external-agents:read` plus Project read access |
| External Agents writes | Authorized API key, or delegated `external-agents:write` plus Project management access |

For delegated terminal access, follow [CLI Authentication](/docs/cli/authentication). Application ingestion uses an API key without browser login. A read-only MCP tool does not narrow the credential's permission elsewhere.

## Access failures

For `401`, check the credential's presence, validity and API origin. For `403`, check the workspace, Project, capability, restrictions and delegated scope. Organization IP restrictions can apply. Correct access through normal administration.
## Related reference pages

{% related-card-grid title="Related reference pages" %}
- [Public API](/docs/public-api)
- [API Conventions](/docs/public-api/conventions)
- [API Keys and Delegated Access](/docs/admin-console/api-keys)
{% /related-card-grid %}
