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

# Authentication and scopes

> How security partners authenticate with the Compliance API, and how customers grant them access to a workspace

This page is for security partners. It explains what you sign, what the customer provides, and what each credential allows.

Every request carries two independent credentials, answering two different questions.

* **The customer's workspace API key** says which workspace you may read. The customer mints it and hands it to you. Base44 isn't part of that exchange.
* **Your partner assertion** is a short-lived JWT you sign. It says who is calling, and it's the only thing that identifies you, because a customer may share their own key with whoever they choose.

```bash theme={null}
GET /api/v1/compliance/ws_abc123/apps
api_key: b44k_xxxxxxxxxxxxxxxx
Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6ImsxIn0.eyJpc3MiOi...
```

A leaked customer key alone can't call this API, and your credential alone can't reach any workspace.

## Registering as a partner

Base44 issues you no secret. You generate a key pair, keep the private half, and register the public half with Base44. Registration is one-time and covers every customer you go on to scan.

**To register:**

1. Generate a key pair. Base44 accepts an EC key on the P-256 curve (signing with ES256) or an RSA key of at least 2048 bits (RS256). The algorithm is derived from the key itself rather than declared, so no other curve or key type registers.
2. Send Base44 your display name, a short slug, a technical contact address for rotation and incident notice, and the contents of `signing.pub`, the PEM block beginning `-----BEGIN PUBLIC KEY-----`. The slug is what the customer's own audit log names as the caller of each scan.
3. Base44 returns your `client_id` (a UUID). It travels in every token you sign as the `iss` claim. It's an identifier rather than a credential. On its own it grants nothing, and it doesn't need protecting the way a secret would.

> [!WARNING]
>
> Never send the private key. The registration path rejects a private-key PEM outright, so storing one is impossible rather than merely forbidden.

### Rotation and revocation

* **Rotation:** You can hold up to three active keys at once, so rotation needs no downtime. Register the new key, start signing with it (set `kid` if you haven't been, and send Base44 its value), confirm your traffic has moved, then ask Base44 to retire the old one. Base44 records a last-used time per key so that confirmation is possible.
* **Revocation:** Ask Base44 and a key is disabled immediately. Partner and key status are checked on every call, so there's no cache to wait out. Suspension is per partner rather than per key. It stops your access across every customer at once, and your keys survive to be reinstated.

## The token you sign

Sign a JWT with the private key whose public half you registered. This contract is published in the API's OpenAPI security scheme, because it's what you build against.

| Claim | Requirement |
| - | - |
| `alg` (header) | `RS256` or `ES256`, pinned before any key is loaded. |
| `kid` (header) | Optional. Set it if you hold more than one registered key. |
| `iss` | Your `client_id`. |
| `aud` | `https://base44.com/api/v1/compliance`, fixed. |
| `iat` | Issue time, in seconds, not milliseconds. |
| `exp` | No more than five minutes after `iat`. |
| `jti` | Unique per token, accepted exactly once. |

The `aud` value is a fixed identifier, not the host you send requests to. Use it exactly as shown, even though API requests go to `app.base44.com`.

All five claims are required, and up to 60 seconds of clock skew is tolerated either way. Sign a fresh token for every request, including every retry. A token signed for another audience isn't replayable here, and a long-lived one is refused rather than honored.

```python theme={null}
import uuid, datetime, jwt

def mint_token(private_pem, client_id):
    now = datetime.datetime.now(datetime.timezone.utc)
    return jwt.encode(
        {
            "iss": client_id,
            "aud": "https://base44.com/api/v1/compliance",
            "iat": int(now.timestamp()),
            "exp": int((now + datetime.timedelta(minutes=2)).timestamp()),
            "jti": str(uuid.uuid4()),
        },
        private_pem,
        algorithm="ES256",
    )
```

The token identifies you. It doesn't narrow what you may read, which is set by the scope on the customer's own key.

Failures are deliberately uninformative. A missing, malformed, unknown, unverifiable, expired, or replayed token all return the same `401`, so a caller can't probe which partners exist. The one exception is a token that verifies but belongs to a suspended partner, which returns `403`, because you're entitled to know your own status.

## The workspace API key

The workspace API key is the customer's credential.

* It has the prefix `b44k_`. A workspace admin generates it in workspace settings, and it's shown once at creation.
* It's scoped to exactly one workspace. A key can never read another, and the workspace in the path must match the key.
* You pass it in the `api_key` request header, not `Authorization`, which carries your partner assertion instead.

## Scopes

The customer chooses, at key creation, which data families the key can read.

| Scope | Grants | Sensitivity |
| - | - | - |
| `compliance:inventory:read` | Workspace configuration, apps, domains, API key metadata, entities, functions, secrets metadata, integrations, agents | Baseline posture inventory |
| `compliance:members:read` | Member directory (emails, roles, MFA status, activity) | People data |
| `compliance:findings:read` | Security findings | Vulnerability data |

`compliance:members:read` and `compliance:findings:read` each require `compliance:inventory:read` to also be granted.

Scopes are fixed at key creation, so changing access means rotating the key.

If the key is limited to selected apps, that limit doesn't apply here. These endpoints always report the whole workspace. Consent is by data family only, deliberately not per app and not per field, because a per-app filtered inventory would be a partial posture picture presented as a complete one.

Partial grants are visible, never silent. Calling an endpoint outside the key's scopes returns a typed `403` `permission_error` naming the scope the endpoint needs. Render it as "not granted by customer" per data family rather than presenting a thinner report as complete.

## Hard exclusions

The API never returns the following under any scope:

* Conversation content of any kind, including builder chats and agent transcripts.
* Entity records.
* File contents.
* Secret values.
* OAuth tokens and client secrets.
* Raw API keys or their hashes.
* End-user (app-user) identities.

A future, separately announced content scope may make conversation content reachable for eDiscovery and DLP consumers. No existing key would silently gain it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.