---
title: "API Keys"
slug: help-api-keys
section: In-App Help
status: stable
description: "In-app help · Admin > Integrations > API Keys"
last-updated: 2026-10-07
---

# API Keys

In-app help · Admin > Integrations > API Keys

## What Are API Keys?

API Keys allow external systems to authenticate with the Globus platform. Each key is tied to a specific scope and permission set, ensuring that integrations only access the data they need. Keys are used by partner portals, n8n automations, reporting tools, and any system that communicates with the Globus API.

> **Integration Architecture**
>
> For the full integration contract design, see the Integration Contract page. API Keys are one authentication method within that broader architecture.

## Key Types

API keys are not divided into built-in types: each key has a name, a scope list, an optional partner organization and an optional expiry. The patterns below are typical uses, and rate limits are not configured per key.

| Example pattern | Use Case | Typical scope | Rate Limit |
| --- | --- | --- | --- |
| Portal sync | Partner portal ↔ Globus data sync | Partner-scoped (only their applicants) | Not configured per key |
| Automation | n8n workflows, scheduled jobs, webhooks | Configurable per-module permissions | Not configured per key |
| Reporting | BI tools, analytics dashboards, exports | Read-only, brand- or program-scoped | Not configured per key |
| Administration | System administration, bulk operations | Full API access (Super Admin only) | Not configured per key |

## Creating an API Key

Navigate to Admin > Integrations > API Keys > Create Key.

Two permission keys gate the key management screens (both restricted): `api_keys.view` (view API keys and usage logs; list, detail and usage routes under /api/admin/api-keys) and `api_keys.manage` (create, edit, revoke and rotate keys; requires a reason). The same `api_keys.manage` permission gates webhook subscription create, edit, delete, test and secret rotation.

> **Key Shown Once**
>
> The raw API key is displayed only once at creation time. Globus stores a hashed version internally. If you lose the key, you must revoke it and create a new one.

The scope picker in the key management UI is driven by the api_scope lookup table. This table is now backfilled by migration (Aug 11 2026 release capsule), so deployed environments (QA, production) can grant recently-added enforcement-layer scopes without waiting for a UI rebuild.

The catalog now offers the scopes that routes actually enforce, so they no longer need the wildcard scope or a direct database edit: host settings, agreements and housing write (settings.read/write, agreements.read/write, housing.write), job_orders.write, compliance (compliance.write, compliance.bundles.read/manage), config (config.export/import/publish), finance (fees.view, finance.invoices.read/create, finance.payment_imports.execute, finance.notification_deliveries.execute), integrations (integrations.hubspot.inbound, integrations.job_orders.read/write), and others. Two scopes that no route ever enforced, job_orders.detail and vacancies.read, were retired: deactivated by migration so they no longer appear in the picker, while existing keys that already carry the string and the audit history still resolve it.

The PII-unmask scopes are self-service in the picker: sensitive_data.view (labelled Unmask Sensitive Data) returns SSNs and passport numbers unredacted instead of masked, and applicant.pii.read is its legacy alias. Each unmasked read writes an applicant.pii.unmasked audit row.

A key whose non-wildcard scopes are all host-portal scopes (host.* plus settings.read/write, agreements.read/write, housing.write, job_orders.*, and the portal proxy scopes) is blocked from /api/admin/*, and adding the wildcard does not lift that. housing.read is not host-only, so back-office keys holding it still reach admin housing routes.

The data_lake.read scope (Data Lake (read)) is a single read-only scope for external data-lake connectors. It is accepted as an alternative to a route's own scope on an approved shortlist of read and list endpoints (applicants, applications, stage history, programs, seasons and season capacity, brands, placements and reservations, job orders and their matches, partner organizations), not as an extra requirement. A key whose entire scope set is data_lake.read plus the PII-unmask scopes and partner:, program:, season: or brand: restriction tokens can only make GET, HEAD or OPTIONS requests; any write is refused with a 403 (read_only_scope). The partner, program, season and brand tokens narrow the rows returned, and applicant PII stays masked unless sensitive_data.view is also granted.

**Scope to permission mapping.** A key is authorised by the staff permissions its scopes map to (`API_SCOPE_TO_PERMISSIONS` in `server/rbac.ts`); the `*` scope grants every permission, and a scope string not in the map grants nothing. Selected mappings:

- `applicants.read` and `applicants.view`: `applicants.list.view`, `applicant.profile.view`, `applicant.view`, `dashboard.view`. `applicants.nudge`: `applicant.journey.update`. `applicants.notes`: `applicant.notes.edit`.
- `applications.read`: `applicants.list.view`, `applicant.profile.view`, `applications.view`, `compliance.view`, `dashboard.view`.
- `documents.read`: `document.view`, `documents.queue.view`. `documents.write` adds `document.verify` and `document.reject`. `enrollments.read`: `dashboard.view`.
- `finance.invoices.create`: `fees.trigger`.
- `admin.read`: `dashboard.view`, `users.manage`, `roles.view`, `audit_log.view`. `admin.write`: `dashboard.view`, `users.manage`, `roles.view`, `role_templates.manage`, `programs.manage`, `seasons.manage`, `operational_cycles.manage`, `system_settings.manage`, `guide.manage`.
- `sevis.write`: the SEVIS ready pool, batch (create, confirm issue, results, retry), DS resubmission, real-time search, validation and status permissions plus `dashboard.view`.
- `placements.write`: `reservations.view`, `reservations.manage`, `placements.view`, `placements.manage`.
- `compliance.read`: `compliance.view`, `compliance.gates.override`, `compliance.rules.author`.
- `dashboard.read`: `dashboard.view`, `document.verify`, `admin.access`, `travel.view`, `travel.review`.

## Security Best Practices

Never embed API keys in frontend code or client-side applications / Store keys in environment variables or a secrets manager — never in source control / Use the narrowest scope possible — prefer partner-scoped over unscoped keys / Set an expiration date and rotate keys before they expire / Use separate keys for each integration — don’t share a single key across systems / Monitor the usage dashboard for anomalous patterns (unusual volume, unexpected endpoints)
## Rotating & Revoking Keys

Key rotation ensures that compromised or aging keys are replaced without downtime.

## Usage Monitoring

The API Keys dashboard shows real-time usage metrics for each key:

On a failed API key call, the usage log now records the specific failure reason alongside the HTTP status code (Aug 11 2026 release capsule). Failure reasons distinguish an expired key, insufficient scope, and a wrong host context — so support investigations can tell these apart at a glance rather than seeing only the status code. Each failed call stores an error code and an error message; for a scope rejection the message lists the scopes required and the scopes the key holds. The Usage Logs dialog shows this in an Error column (successful calls show a dash; rows logged before this change show "No detail recorded").

**Authentication error codes.** Failures use the standard error envelope with `error.code`.

- `API_KEY_REQUIRED` (401): the endpoint accepts a scoped API key only (no staff session) and no `x-api-key` header was sent.
- `SESSION_REQUIRED` (401): an `x-api-key` header was sent to a session-only endpoint (for example config import), which never accepts keys. The key is not validated.
- `SERVICE_UNAVAILABLE` (503): on `/api/admin/*` the lookup that checks whether the key is bound to a host organization failed, so the request fails closed (`details.reason` is `host_lookup_failed`). Retry shortly.
- `MFA_SETUP_REQUIRED` (403): a signed-in staff session whose mandatory MFA enrolment is pending. Every route guarded by session authentication, except `/api/auth/mfa/*`, is refused until setup completes.
- `ENTRA_SSO_REQUIRED` (403): returned by `POST /api/auth/login` after credentials are verified, when Microsoft SSO is configured and the account is linked to Microsoft. The user must use Sign in with Microsoft; god-mode (system.admin) holders are exempt.

**Usage endpoint.** `GET /api/admin/api-keys/:id/usage` (permission `api_keys.view`) returns the key usage log rows, newest first, as a page. The optional `limit` query parameter defaults to 100 and is capped at 500. An unknown key id returns 404 `NOT_FOUND`.

**Usage log table.** Rows are stored in `api_key_usage_logs`: `api_key_id` (foreign key to `api_keys`), `endpoint`, `method`, `status_code`, `error_code` and `error_message` (both null on a successful call), `ip_address`, `user_agent`, `request_scope` and `used_at`. The table is indexed on `api_key_id` and `used_at`.

### Scope to Permission Mapping

A key passes a staff permission gate when any of its scopes maps to the required permission, and the `*` scope passes every permission gate (a key must still hold whatever scope the route itself names). The mapping lives in `API_SCOPE_TO_PERMISSIONS` in `server/rbac.ts`. These scopes map as follows:

| Scope | Staff permissions it maps to |
| --- | --- |
| `webhooks.read` | `dashboard.view` |
| `workflows.read` | `programs.manage`, `dashboard.view`, `audit_log.view` |
| `document.read` | `document.view` |
| `stages.read` | `applications.edit` |
| `capacity.read` | `capacity.view`, `capacity.rules.manage`, `capacity.adjust` |
| `checkins.read` | `applicant.view`, `applicant.manage` |
| `programs.read` | `programs.manage` |
| `programs.write` | `programs.manage`, `compliance.rules.author`, `compliance.rules.publish`, `compliance.rules.assign` |
| `seasons.write` and `operational_cycles.write` | `seasons.manage`, `operational_cycles.manage` (each scope maps to both) |
| `forms.read` | `users.manage`, `applications.edit` |
| `notifications.read` | `audit_log.view`, `notifications.view`, `notifications.send`, `notifications.manage`, `applicant.manage`, `applicant.view` |
| `site_visits.read` | `site_visits.manage` |
| `host.read`, `host.write`, `extensions.read`, `extensions.write`, `extension_requests.read` | `dashboard.view` (each scope) |
| `reservations.read` | `reservations.view`, `reservations.manage` |

## Common Questions
