Stitchflow
Backstage logo

Backstage User Management API Guide

API workflow

How to automate user lifecycle operations through APIs with caveats that matter in production.

UpdatedMar 18, 2026

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 APIYes
Auth methodBearer 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 URLOfficial docs
SCIM availableNo
SCIM plan requiredN/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

  1. Configure an auth provider in app-config.yaml under auth.providers (e.g., github, google, okta).
  2. For user sessions, the frontend obtains a Backstage identity token via the auth plugin; pass it as 'Authorization: Bearer ' on API requests.
  3. 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 '.
  4. 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:/' or shorthand ''.
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

  1. 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).
  2. Configure the provider in app-config.yaml with IdP credentials and org/tenant details.
  3. Register the provider in the catalog builder in packages/backend/src/plugins/catalog.ts.
  4. On startup (and on the configured schedule), the provider fetches users/groups from the IdP and emits User and Group entities into the catalog.
  5. 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

  1. Call GET /api/catalog/entities/by-name/user/default/{username} with a valid Bearer token.
  2. Read spec.memberOf from the response to get the list of group entity refs.
  3. For each group ref, call GET /api/catalog/entities/by-name/group/default/{groupname} to retrieve group details.
  4. 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

  1. Identify the user's catalog UID via GET /api/catalog/entities/by-name/user/default/{username} and extract metadata.uid.
  2. Call DELETE /api/catalog/entities/by-uid/{uid} with a service Bearer token that has catalog-entity:delete permission.
  3. Remove or update the source YAML file or IdP record so the entity provider does not re-emit the user on the next sync.
  4. 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.

Every app coverage, including apps without APIs
60+ app integrations plus browser automation for apps without APIs
IT graph reconciliation across apps and your IdP
Less than a week to launch, maintained as APIs and admin consoles change
SOC 2 Type II. ~2 hours of your team's time

UpdatedMar 18, 2026

* Details sourced from official product documentation and admin references.

Keep exploring

Related apps

Abnormal Security logo

Abnormal Security

API Only
AutomationAPI only
Last updatedMar 2026

Abnormal Security is an enterprise email security platform focused on detecting and investigating threats such as phishing, account takeover (ATO), and vendor email compromise. It does not support SCIM provisioning, which means every app in your stack

ActiveCampaign logo

ActiveCampaign

API Only
AutomationAPI only
Last updatedFeb 2026

ActiveCampaign uses a group-based permission model: every user belongs to exactly one group, and all feature-area access (Contacts, Campaigns, Automations, Deals, Reports, Templates) is configured at the group level, not per individual. The default Adm

ADP logo

ADP

API Only
AutomationAPI only
Last updatedFeb 2026

ADP Workforce Now is a mid-market to enterprise HCM platform that serves as the HR source of record for employee data — payroll, benefits, time, and talent. User access is governed by a hybrid permission model: predefined security roles (Security Maste