Summary and recommendation
The Backstage catalog API is a REST interface served at operator-defined base URL `http://<your-backstage-host>/api/catalog`.
There is no SaaS endpoint.
Authentication uses short-lived Bearer JWTs issued by the configured auth provider for user sessions, or static shared secrets defined under `backend.auth.keys` in `app-config.yaml` for service-to-service calls.
There is no SCIM 2.0 endpoint;
user provisioning is handled exclusively through entity provider plugins or YAML-defined catalog locations.
The catalog API is primarily a read and query interface.
Direct REST mutation of User entities is not the standard pattern entities written via the API will be overwritten on the next provider sync cycle if a live source still emits them.
Rate limiting is not enforced natively;
it must be implemented at the infrastructure layer (reverse proxy or API gateway).
Pagination uses a cursor model: the `after` param accepts the cursor token returned in the `Link: <url>;
rel="next"` response header.
Default page size is 20;
no documented maximum.
There is no total count field in the response.
API quick reference
| Has user API | Yes |
| Auth method | Bearer token (Backstage user identity token or static service token). Backstage supports pluggable auth providers (GitHub, Google, Okta, etc.) that issue short-lived JWTs. Service-to-service calls use static tokens configured in app-config.yaml under backend.auth.keys. |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | N/A |
Authentication
Auth method: Bearer token (Backstage user identity token or static service token). Backstage supports pluggable auth providers (GitHub, Google, Okta, etc.) that issue short-lived JWTs. Service-to-service calls use static tokens configured in app-config.yaml under backend.auth.keys.
Setup steps
- Configure an auth provider in app-config.yaml under auth.providers (e.g., github, google, okta).
- For user sessions, the frontend obtains a Backstage identity token via the auth plugin; pass it as 'Authorization: Bearer
' on API requests. - For service-to-service (machine) access, define a static shared secret under backend.auth.keys in app-config.yaml and pass it as 'Authorization: Bearer
'. - Ensure the catalog backend plugin is running and accessible at /api/catalog.
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| apiVersion | string | Always 'backstage.io/v1alpha1' or 'backstage.io/v1beta1' | required | required | Fixed value for User kind. |
| kind | string | Must be 'User' | required | required | Identifies entity type in catalog. |
| metadata.name | string | Unique identifier / username within the namespace | required | immutable | Lowercase, URL-safe string. |
| metadata.namespace | string | Catalog namespace; defaults to 'default' | optional | immutable | Used to scope entity uniqueness. |
| metadata.title | string | Human-readable display name | optional | optional | Shown in UI instead of metadata.name when present. |
| metadata.description | string | Free-text description of the user | optional | optional | |
| metadata.annotations | map<string,string> | Key-value annotations, e.g. github.com/user-login | optional | optional | Used by integrations to link external identities. |
| metadata.labels | map<string,string> | Key-value labels for filtering/querying | optional | optional | |
| metadata.tags | string[] | Short tags for categorization | optional | optional | |
| metadata.links | object[] | External links associated with the user | optional | optional | Each link has url, title, icon fields. |
| spec.profile.displayName | string | Full display name of the user | optional | optional | |
| spec.profile.email | string | Primary email address | optional | optional | Used for identity matching by some auth providers. |
| spec.profile.picture | string | URL to avatar/profile picture | optional | optional | |
| spec.memberOf | string[] | List of Group entity refs this user belongs to | optional | optional | Format: 'group: |
| relations | object[] | Computed relations (e.g., memberOf, hasMember) populated by the catalog processor | read-only | read-only | Not set by caller; derived by catalog. |
Core endpoints
List entities (filter to User kind)
- Method: GET
- URL:
/api/catalog/entities?filter=kind=User&limit=20 - Watch out for: Response is a flat JSON array, not a wrapped object. Pagination cursor is returned in the 'Link' response header (rel=next) or via the after query param.
Request example
GET /api/catalog/entities?filter=kind=User&limit=20
Authorization: Bearer <token>
Response example
[
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "User",
"metadata": {"name": "jdoe", "namespace": "default"},
"spec": {"profile": {"email": "jdoe@example.com"}, "memberOf": ["team-a"]}
}
]
Get single entity by kind/namespace/name
- Method: GET
- URL:
/api/catalog/entities/by-name/user/default/jdoe - Watch out for: Returns 404 if the entity does not exist or has not yet been ingested by a catalog processor.
Request example
GET /api/catalog/entities/by-name/user/default/jdoe
Authorization: Bearer <token>
Response example
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "User",
"metadata": {"name": "jdoe"},
"spec": {"profile": {"email": "jdoe@example.com"}}
}
Get entity by UID
- Method: GET
- URL:
/api/catalog/entities/by-uid/{uid} - Watch out for: UID is assigned by the catalog on ingestion and is stable across renames only if the entity is not deleted and re-created.
Request example
GET /api/catalog/entities/by-uid/a1b2c3d4-...
Authorization: Bearer <token>
Response example
{
"apiVersion": "backstage.io/v1alpha1",
"kind": "User",
"metadata": {"uid": "a1b2c3d4-...", "name": "jdoe"}
}
Register / upsert entity (via location)
- Method: POST
- URL:
/api/catalog/locations - Watch out for: Backstage does not expose a direct POST /entities endpoint. Users are ingested via catalog locations (YAML files) or entity providers. Direct mutation is not the standard pattern.
Request example
POST /api/catalog/locations
Authorization: Bearer <token>
Content-Type: application/json
{"type": "url", "target": "https://github.com/org/repo/blob/main/users/jdoe.yaml"}
Response example
{
"location": {"id": "loc-uuid", "type": "url", "target": "..."},
"entities": []
}
Delete entity by UID
- Method: DELETE
- URL:
/api/catalog/entities/by-uid/{uid} - Watch out for: Deleting an entity only removes it from the catalog store. If the source location (YAML/provider) still exists, the entity will be re-ingested on the next refresh cycle.
Request example
DELETE /api/catalog/entities/by-uid/a1b2c3d4-...
Authorization: Bearer <token>
Response example
HTTP 204 No Content
Refresh entity (trigger re-ingestion)
- Method: POST
- URL:
/api/catalog/refresh - Watch out for: Requires the caller to have catalog-entity:refresh permission if the permission framework is enabled.
Request example
POST /api/catalog/refresh
Authorization: Bearer <token>
Content-Type: application/json
{"entityRef": "user:default/jdoe"}
Response example
HTTP 200 OK
{}
Query entities (POST-based filter)
- Method: POST
- URL:
/api/catalog/entities/by-refs - Watch out for: Items array is positionally aligned to the input entityRefs array. Missing entities are returned as null at the corresponding index.
Request example
POST /api/catalog/entities/by-refs
Authorization: Bearer <token>
Content-Type: application/json
{"entityRefs": ["user:default/jdoe", "user:default/asmith"]}
Response example
{
"items": [
{"apiVersion": "backstage.io/v1alpha1", "kind": "User", "metadata": {"name": "jdoe"}},
null
]
}
List Group entities (for membership context)
- Method: GET
- URL:
/api/catalog/entities?filter=kind=Group&limit=20 - Watch out for: Group membership is bidirectional: spec.members on Group and spec.memberOf on User. The catalog reconciles both directions into computed relations.
Request example
GET /api/catalog/entities?filter=kind=Group&limit=20
Authorization: Bearer <token>
Response example
[
{
"kind": "Group",
"metadata": {"name": "team-a"},
"spec": {"type": "team", "members": ["jdoe"]}
}
]
Rate limits, pagination, and events
Rate limits: Backstage is self-hosted open-source software. There are no built-in rate limits enforced by the catalog API. Rate limiting is the operator's responsibility and must be implemented at the infrastructure layer (e.g., reverse proxy, API gateway).
Rate-limit headers: No
Retry-After header: No
Rate-limit notes: No native rate-limit headers or Retry-After behavior documented in official Backstage sources.
Pagination method: cursor
Default page size: 20
Max page size: Not documented
Pagination pointer: limit / after (cursor token returned in response)
Webhooks available: No
Webhook notes: Backstage does not provide native outbound webhooks for catalog entity events. There is no built-in webhook subscription mechanism in the core catalog API.
Alternative event strategy: Operators can implement event-driven patterns using the experimental @backstage/plugin-events-backend plugin, which provides an internal event bus. External notification requires custom event subscribers or polling the catalog API.
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: N/A
- Endpoint: Not documented
Limitations:
- Backstage has no native SCIM 2.0 endpoint.
- User provisioning is handled via catalog entity providers (e.g., GitHub Org, LDAP, Microsoft Graph, Okta org provider plugins) that sync users from an IdP into the catalog.
- There is no SCIM receiver built into Backstage core or any officially documented SCIM compatibility layer.
Common scenarios
Three primary integration patterns are supported by the catalog API and entity provider system:
IdP sync via provider plugins: Install the appropriate module (@backstage/plugin-catalog-backend-module-okta, -module-github-org, or -module-msgraph), configure credentials and org/tenant details in app-config.yaml, and register the provider in the catalog builder.
The provider emits User and Group entities on startup and on schedule.
Verify with GET /api/catalog/entities?filter=kind=User.
Note: each provider has its own annotation conventions;
manual API edits to provider-owned entities are overwritten on the next sync.
User and group membership resolution: Call GET /api/catalog/entities/by-name/user/default/{username} and read spec.memberOf for group entity refs, or traverse the relations array (type memberOf) populated by the catalog processor.
Relations are computed asynchronously - a freshly ingested entity may have empty relations until POST /api/catalog/refresh is called.
Offboarding via API: Retrieve the user's UID from metadata.uid, call DELETE /api/catalog/entities/by-uid/{uid} with a service token holding catalog-entity:delete permission, then remove or update the source YAML or IdP record.
The DELETE call alone is insufficient if the source location or provider still emits the entity - it will be re-ingested on the next refresh cycle.
For teams building an identity graph across their engineering org, the spec.memberOf / spec.members bidirectional relation model and the relations array on each entity provide the raw material.
The catalog processor reconciles both directions into computed relations, enabling traversal of the full org graph via the catalog API.
Sync users from an external IdP into the Backstage catalog
- Install the appropriate entity provider plugin (e.g., @backstage/plugin-catalog-backend-module-okta, @backstage/plugin-catalog-backend-module-github-org, or @backstage/plugin-catalog-backend-module-msgraph).
- Configure the provider in app-config.yaml with IdP credentials and org/tenant details.
- Register the provider in the catalog builder in packages/backend/src/plugins/catalog.ts.
- On startup (and on the configured schedule), the provider fetches users/groups from the IdP and emits User and Group entities into the catalog.
- Verify ingestion by calling GET /api/catalog/entities?filter=kind=User.
Watch out for: Each provider plugin has its own configuration schema and annotation conventions. Users ingested by a provider are owned by that provider; manual edits via the API will be overwritten on the next sync cycle.
Look up a user entity and resolve group memberships
- Call GET /api/catalog/entities/by-name/user/default/{username} with a valid Bearer token.
- Read spec.memberOf from the response to get the list of group entity refs.
- For each group ref, call GET /api/catalog/entities/by-name/group/default/{groupname} to retrieve group details.
- Alternatively, use the relations array on the User entity (relation type 'memberOf') which is populated by the catalog processor.
Watch out for: Relations are computed asynchronously after ingestion. A freshly registered user entity may not yet have populated relations; call POST /api/catalog/refresh to trigger re-processing.
Remove a user from the catalog when they leave the organization
- Identify the user's catalog UID via GET /api/catalog/entities/by-name/user/default/{username} and extract metadata.uid.
- Call DELETE /api/catalog/entities/by-uid/{uid} with a service Bearer token that has catalog-entity:delete permission.
- Remove or update the source YAML file or IdP record so the entity provider does not re-emit the user on the next sync.
- If using an IdP provider plugin, deactivating the user in the IdP is sufficient; the provider will stop emitting the entity and the catalog will mark it as orphaned and eventually remove it (depending on orphan strategy configuration).
Watch out for: If the source location or provider still emits the entity, it will be re-ingested. The DELETE API alone is not sufficient for permanent removal when a live source exists.
Why building this yourself is a trap
The most common integration mistake is treating the catalog API as a writable user directory. It is not. Entities created or modified via direct API calls will be silently overwritten by the next provider sync or YAML location refresh if a live source exists for that entity.
Any automation that writes User entities must either own the source location exclusively or operate through the provider plugin layer.
A second trap is assuming permission enforcement is active. By default, Backstage ships with no permission policy; all authenticated callers have full catalog read/write access.
Service tokens issued against a deployment without a permission policy will succeed on every operation - including deletes - which can produce destructive outcomes in automated pipelines. Always verify whether a PermissionPolicy is deployed before assuming access controls are in effect.
Token lifetime is a third caveat: user-session JWTs are short-lived and their expiry is governed by the configured auth provider plugin, not by Backstage core. Service-to-service static tokens do not expire but must be rotated manually. There is no token introspection endpoint in the catalog API.
Integrations that cache tokens must implement their own refresh logic aligned to the provider's JWT lifetime.
Automate Backstage workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.