Skip to main content

View source on GitHub

Create an organization, provision users into it, and get a per-user API key to act on each user’s behalf, all as the superadmin of an OpenHands Enterprise instance. No UI and no interactive sign-up are involved. This is the building block for OEM integrations, where your own user-management system owns the user and organization lifecycle and mirrors it into OpenHands.
This example is Enterprise-only. The endpoints do not exist on OpenHands Cloud or the open-source build. The provisioning endpoint is also off by default, so an operator must enable it (see Enabling the Feature).

How It Works

Every call is made as the superadmin. The api_key that comes back is what your system stores and uses afterwards.
  1. POST /api/organizations creates the org. Only a superadmin can do this, and the creator is not added as a member.
  2. POST /api/organizations/provision-user creates a user in that org. The target org goes in the X-Org-Id header, and the call bypasses email verification, terms of service, and OAuth.
  3. Use the returned api_key as a Bearer token to act on that user’s behalf.
The provision-user response holds the new user’s email, a password (generated if you did not supply one, and returned only on a true create), and an api_key bound to the target org.

Idempotency

provision-user is idempotent. Re-running it for an email that already exists does not fail or duplicate the account. The server skips the create steps, ensures org membership, and resolves an API key. The action field and the HTTP status tell you which branch it took: The existing Keycloak password is never rotated, so password is null on both idempotent paths. By default the endpoint returns the user’s existing API key. Pass reissue_api_key: true (or --reissue-api-key on the script) to delete the old key and mint a fresh one. Use this to recover a user who lost their key.

Prerequisites

  • An OpenHands Enterprise instance with user provisioning enabled (see Enabling the Feature).
  • The superadmin bearer token for that instance. On a fresh install, the superadmin is the first user to authenticate.
  • pip install requests
A superadmin can list its peers with GET /api/admin/super-admins. That endpoint requires superadmin, unless user provisioning is enabled on OHE 1.62.0 or later, when any signed-in user can call it. There is no self-service “am I the superadmin?” endpoint. The org-admin example shows how to find the superadmin and grant the role to more users.

Run It

Run these from the provision-users/ directory.
With no other arguments the script runs a complete demo. It creates a throwaway org and one user, verifies the user’s key with GET /api/organizations/{org_id}/me, then deletes both again.
The password and API key are masked in the output. Pass --show-secrets to print them in full.
A superadmin has no org-scoped powers, so deleting an org needs the key of an org owner. When none of the demo users is an owner, the script provisions a short-lived cleanup-…@example.com owner for that step and deletes it with the others.
To provision real users, name the org or the users. The script then keeps everything it creates.

The Two Calls That Matter

Create the org. The superadmin is not added as a member:
Provision a user into it. The target org travels in the X-Org-Id header, not the URL:
role is one of member (default), admin, or owner. You can also pass an optional password (it must satisfy the realm policy), api_key_name, and reissue_api_key.

Enabling the Feature

The endpoint is registered only when the operator turns it on. Both install types set USER_PROVISIONING_ENABLED=true on the app server: The Enable OEM User Creation Flow checkbox under Advanced Options in the admin console Config tab When it is disabled, POST /api/organizations/provision-user returns 404. Org creation and superadmin management are always available.

Troubleshooting

You are calling the wrong endpoint. That error comes from the internal service-to-service routes under /api/service/*, such as POST /api/service/users/{user_id}/orgs/{org_id}/api-keys. They authenticate with the X-Service-API-Key header, checked against the operator-provisioned AUTOMATIONS_SERVICE_KEY shared secret. That key is meant for the internal automations service, not for admins.User provisioning is a different flow. Call POST /api/organizations/provision-user with an Authorization: Bearer <superadmin-token> header and X-Org-Id. It never uses X-Service-API-Key. X-Session-API-Key (per-sandbox agent-server auth, used in other examples) is also distinct from the plain Bearer token this endpoint expects.
The feature is disabled. See Enabling the Feature.
The caller lacks the PROVISION_USER permission (org owner or admin, or an instance super role), or you targeted a personal workspace. Provisioning into a personal workspace is rejected.

Security

The provision-user response is a secret. The password is returned only there, because the endpoint bypasses the email-based set-password flow, and the api_key grants access as the new user. Do not log the response, hand the credential to the user out-of-band, and always use TLS. The script masks both values unless you pass --show-secrets.
  • Guard the superadmin token. Anyone holding it can create orgs, mint users, and grant or revoke superadmin. Treat it like a root credential.
  • A superadmin does not inherit org-scoped powers (secrets, billing, LLM settings, org deletion) in the orgs it creates.

APIs Used

service-account-github-pat

Act for many users from one account with per-conversation credentials

per-conversation-secrets

Inject secrets into a single conversation

User provisioning endpoint (OpenHands #14864)

The provisioning endpoint

Super roles (OpenHands #14937)

Instance-level superadmin roles

Idempotent re-provisioning (enterprise #117)

reissue_api_key, action, and 200 vs 201