Managing OAuth clients

Organization administrators can create and manage OAuth clients for their organization directly via the REST API. A client identifies an application, not an organization: the tenant_mode chosen at creation is immutable and controls which ezeep users can sign in to the client — single (default) restricts sign-in to users of the owning organization, multi allows users from any ezeep organization.

All endpoints in this reference require an organization-admin access token in the Authorization header. See Obtaining a ClientID for your Application for the settings you should decide before creating a client (client type, OAuth flow, redirect URIs, scopes).

Anonymous requests return 401 Unauthorized; requests from non-admin users return 403 Forbidden. A request for a client that belongs to a different organization returns 404 Not Found — the existence of other organizations’ clients is never revealed.

Create a Public Client

Creates a public OAuth client (mobile, desktop, SPA, CLI, device). Public clients use authorization_code with PKCE and have no client secret.

POST "https://account.ezeep.com/v1/oauth-clients/public/"
Attribute Type Required Description
name string yes Human-readable name shown on the consent screen.
redirect_urls string array yes Allowed redirect URIs. Max 20. Each URL must be HTTPS, http://localhost, or a reverse-DNS native scheme (e.g. com.example.app://cb). No wildcards, no fragments, no userinfo.
external_description string no Description shown on the consent screen.
allowed_scopes string no Space-separated scopes. External families only: printing.*, accounts.*, reporting.*, azure.*, webhooks.*, openid, profile, email.
tenant_mode string no single (default) or multi. Immutable after creation.

Public clients enforce PKCE and support the authorization_code grant only; no client_secret is issued. The grant_types field is not accepted in the request body for public clients.

Example Request

curl -X POST "https://account.ezeep.com/v1/oauth-clients/public/" \
     --header "Content-Type: application/json" \
     --header "Authorization: Bearer <Org-Admin Access Token>" \
     --data '{
         "name": "Acme Print SPA",
         "redirect_urls": ["https://app.acme.example/oauth/callback"],
         "external_description": "Acme document printing for the web",
         "allowed_scopes": "printing openid profile",
         "tenant_mode": "single"
     }'

Example Response (201 Created)

{
    "client_id": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789AbCd",
    "name": "Acme Print SPA",
    "external_description": "Acme document printing for the web",
    "redirect_urls": ["https://app.acme.example/oauth/callback"],
    "allowed_scopes": ["printing", "openid", "profile"],
    "grant_types": ["authorization_code"],
    "client_type": "public",
    "tenant_mode": "single"
}

Create a Confidential Client

Creates a confidential OAuth client (server-side backend that can protect a secret). Confidential clients receive a client_secret that is shown once in this response and cannot be retrieved later — store it in a secure backend immediately.

POST "https://account.ezeep.com/v1/oauth-clients/confidential/"
Attribute Type Required Description
name string yes Human-readable name shown on the consent screen.
grant_types string array yes Exactly one of authorization_code or pairing_code.
redirect_urls string array conditional Required for authorization_code. May be empty for pairing-only clients. Same validation as public clients: max 20; HTTPS, http://localhost, or reverse-DNS native scheme; no wildcards, no fragments, no userinfo.
external_description string no Description shown on the consent screen.
allowed_scopes string no Space-separated scopes. External families only: printing.*, accounts.*, reporting.*, azure.*, webhooks.*, openid, profile, email.
tenant_mode string no single (default) or multi. Immutable after creation.

Confidential clients accept exactly one grant type per client. Admins who need both authorization_code and pairing_code should register two clients.

Example Request

curl -X POST "https://account.ezeep.com/v1/oauth-clients/confidential/" \
     --header "Content-Type: application/json" \
     --header "Authorization: Bearer <Org-Admin Access Token>" \
     --data '{
         "name": "Acme Print Backend",
         "grant_types": ["authorization_code"],
         "redirect_urls": ["https://backend.acme.example/ezeep/callback"],
         "external_description": "Acme document printing backend",
         "allowed_scopes": "printing",
         "tenant_mode": "single"
     }'

Example Response (201 Created)

{
    "client_id": "ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210zYxW",
    "client_secret": "s3cr3t_shown_once_store_immediately_a1b2c3d4e5f6g7h8i9j0",
    "name": "Acme Print Backend",
    "external_description": "Acme document printing backend",
    "redirect_urls": ["https://backend.acme.example/ezeep/callback"],
    "allowed_scopes": ["printing"],
    "grant_types": ["authorization_code"],
    "client_type": "confidential",
    "tenant_mode": "single"
}

The client_secret is returned only in this response. It cannot be retrieved through any subsequent request. If it is lost, delete the client and create a new one.

Validation Rules

The following rules apply to both public and confidential clients.

Redirect URLs

  • Must be HTTPS, http://localhost (any port and path), or a reverse-DNS native scheme (e.g. com.example.app://callback).
  • Wildcards, URL fragments (#...), and userinfo (user:pass@...) are rejected.
  • Maximum of 20 URLs per client.

Scopes

  • On input: space-separated string, maximum length 2048 characters. On output: JSON array of scope strings (this asymmetry is intentional; the input shape preserves the historical column shape, the output shape matches redirect_urls and grant_types).
  • Only external scope families are accepted: printing.*, accounts.*, reporting.*, azure.*, webhooks.*, openid, profile, email.
  • Any scope starting with internal. is rejected with 400 Bad Request and the generic message “One or more requested scopes are not permitted via API.” — the rejected scope is logged server-side and never echoed back in the response.
  • NFKC Unicode normalization is applied before validation, so scope strings that use lookalike characters (e.g. ɪnternal.printing) are also rejected.

Tenant mode

Tenant mode is set at creation and cannot be changed afterwards; PATCH requests that include tenant_mode are rejected with 400 Bad Request. The field has three observable states:

  • single (default): the client’s owning organization is enforced at sign-in across six of seven grant types (authorization_code, pairing_code, refresh_token, switch_organization, password, external_token). Mismatched requests fail with access_denied at the authorize endpoints and unauthorized_client at the token endpoint. The client_credentials grant is exempt because it has no user and no resolvable organization.
  • multi: users from any ezeep organization can sign in. Note that an organization that owns at least one tenant_mode: 'multi' client cannot be deleted — see the Notes section below.
  • null (legacy, read-only): clients created before this API existed may have tenant_mode: null in GET responses. These clients behave as multi at sign-in and are not reproducible through this endpoint.

Client trust (consent screen)

Clients created through this API are never marked as trusted; the ezeep consent screen is always shown to end users during the authorization flow. Promotion to a consent-skipping (trusted) state is a deliberate staff-only action and is not available through this API.

Refresh-token rotation

Clients created through this API have refresh-token rotation enabled by default: each refresh_token is single-use, and the token-exchange response returns a new refresh_token that must replace the stored value. Rotation is not configurable through this API — contact ezeep support at support@ezeep.com if you need to disable it (for example, for a headless client that cannot persist a rotated token).

Rate and count limits

Two limits apply at creation. Both return 429 Too Many Requests.

  • Per admin: up to 10 client creations per hour. Exceeding this returns 429 with a Retry-After header.
  • Per organization: up to 100 OAuth clients owned by this API. Exceeding this returns 429 immediately; raise the quota by deleting an unused client or by contacting ezeep support.

List Clients

Lists OAuth clients owned by the caller’s organization, both single and multi tenant modes. Results are ordered by created descending (newest first) and then by client_id. Three classes of client are never included: legacy clients with no owning organization, protected clients (created by internal scripts), and partner-scoped clients.

GET "https://account.ezeep.com/v1/oauth-clients/"

Example Request

curl -X GET "https://account.ezeep.com/v1/oauth-clients/" \
     --header "Authorization: Bearer <Org-Admin Access Token>"

Example Response

{
    "count": 2,
    "next": null,
    "previous": null,
    "results": [
        {
            "client_id": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789AbCd",
            "name": "Acme Print SPA",
            "client_type": "public",
            "grant_types": ["authorization_code"],
            "tenant_mode": "single"
        },
        {
            "client_id": "ZyXwVuTsRqPoNmLkJiHgFeDcBa9876543210zYxW",
            "name": "Acme Print Backend",
            "client_type": "confidential",
            "grant_types": ["authorization_code"],
            "tenant_mode": "single"
        }
    ]
}

Client secrets are never returned by this endpoint (or any other endpoint after creation).

Retrieve a Client

Returns the current configuration of a single client.

GET "https://account.ezeep.com/v1/oauth-clients/<client_id>/"

Example Request

curl -X GET "https://account.ezeep.com/v1/oauth-clients/aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789AbCd/" \
     --header "Authorization: Bearer <Org-Admin Access Token>"

Update a Client

Partially updates a client. Only the fields below may be changed; client_type, grant_types, and tenant_mode are immutable and must be set correctly at creation. Unknown fields in the request body are rejected with 400 Bad Request as a mass-assignment safeguard (POST requests silently ignore unknown fields instead; PATCH deliberately does not, so that an attempt to change an immutable field is visible rather than silently dropped).

PATCH "https://account.ezeep.com/v1/oauth-clients/<client_id>/"
Attribute Type Required Description
name string no Human-readable name shown on the consent screen.
external_description string no Description shown on the consent screen.
redirect_urls string array no Same validation rules as at creation.
allowed_scopes string no Space-separated string on input; same validation as at creation. The response body returns allowed_scopes as a JSON array.

Example Request

curl -X PATCH "https://account.ezeep.com/v1/oauth-clients/aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789AbCd/" \
     --header "Content-Type: application/json" \
     --header "Authorization: Bearer <Org-Admin Access Token>" \
     --data '{
         "redirect_urls": [
             "https://app.acme.example/oauth/callback",
             "https://staging.acme.example/oauth/callback"
         ]
     }'

Delete a Client

Deletes a client immediately. Existing access tokens issued to the client remain valid until they expire, but no new tokens can be issued and no refresh will succeed. There is no soft-delete window — a subsequent GET for the same client_id returns 404.

DELETE "https://account.ezeep.com/v1/oauth-clients/<client_id>/"

Example Request

curl -X DELETE "https://account.ezeep.com/v1/oauth-clients/aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789AbCd/" \
     --header "Authorization: Bearer <Org-Admin Access Token>"

Returns 204 No Content on success.

Notes

MCP self-service. The ezeep MCP server’s register_oauth_client tool calls the public-client endpoint on behalf of an organization administrator; see the forthcoming MCP self-service prompt page for a conversational alternative to the raw endpoint.

Clients not visible to this API. Three classes of client are filtered from GET /v1/oauth-clients/ and return 404 on direct access, with their existence not revealed in error responses:

  • Clients issued directly by ezeep support (legacy or on request) — these are system-defined and must continue to be managed via ezeep support at support@ezeep.com.
  • Protected clients created by internal scripts (for example, built-in ezeep applications).
  • Partner-scoped clients.

Organization deletion is blocked by multi-tenant clients. If your organization owns at least one tenant_mode: 'multi' client created through this API, every entry point that deletes the organization (Django admin, the internal admin API, and the partner API) refuses with 409 Conflict and error_code: tenant_mode_multi_clients_exist. Delete or convert the multi-tenant clients to a different organization before attempting to delete the owning organization. Organizations whose only API-created clients are tenant_mode: 'single' are deleted normally — the single-tenant clients cascade.