Skip to content

OAuth ​

Routes are relative to <PUBLIC_BASE_URL>; request and response bodies are JSON unless stated otherwise. Authentication, scopes, pagination and the error format are described in the REST API guides.

GET /api/v1/admin/oauth-apps/{provider} ​

Describe an OAuth application

Returns the callback URL and the scopes even when no application is registered: what the administrator needs to create it in the provider console. Never the secret, only a four-character hint.

Access — Member session or API key with scope admin:read (administrator role required).

Parameters

NameInTypeRequired
providerpathstringyes

Responses

200 — The application state.

FieldTypeRequired
provider"google" | "microsoft"yes
configuredbooleanyes
clientIdstring | nullyes
hasSecretbooleanyes
clientSecretHintstring | nullyes
status"active" | "disabled" | nullyes
redirectUristring (uri)yes
scopesstring[]yes
scopesByCapabilityobjectyes
clientIdPatternstringyes
clientIdHintstringyes
updatedAtstring (date-time) | nullyes
JSON Schema
json
{
  "type": "object",
  "properties": {
    "provider": { "type": "string", "enum": [ "google", "microsoft" ] },
    "configured": { "type": "boolean" },
    "clientId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
    "hasSecret": { "type": "boolean" },
    "clientSecretHint": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
    "status": {
      "anyOf": [
        { "type": "string", "enum": [ "active", "disabled" ] },
        { "type": "null" }
      ]
    },
    "redirectUri": { "type": "string", "format": "uri" },
    "scopes": { "type": "array", "items": { "type": "string" } },
    "scopesByCapability": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [ "mail", "drive", "calendar", "sheets", "files", "sharepoint" ]
      },
      "additionalProperties": { "type": "array", "items": { "type": "string" } }
    },
    "clientIdPattern": { "type": "string", "minLength": 1 },
    "clientIdHint": { "type": "string", "minLength": 1 },
    "updatedAt": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time",
          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
        },
        { "type": "null" }
      ]
    }
  },
  "required": [
    "provider",
    "configured",
    "clientId",
    "hasSecret",
    "clientSecretHint",
    "status",
    "redirectUri",
    "scopes",
    "scopesByCapability",
    "clientIdPattern",
    "clientIdHint",
    "updatedAt"
  ],
  "additionalProperties": false
}

401 — No valid session or API key (auth.unauthenticated).

403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).

429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.

Error codes — oauth.unsupported_provider. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.

PUT /api/v1/admin/oauth-apps/{provider} ​

Register an OAuth application

The client secret enters here and never leaves. Refused when the instance has no encryption key. The client id is checked against the provider’s expected shape. Recorded in the audit log (client id only).

Access — Member session or API key with scope admin:write (administrator role required).

Parameters

NameInTypeRequired
providerpathstringyes

Request body (application/json)

FieldTypeRequired
clientIdstringyes
clientSecretstringyes
status"active" | "disabled"no
JSON Schema
json
{
  "type": "object",
  "properties": {
    "clientId": { "type": "string", "minLength": 1, "maxLength": 512 },
    "clientSecret": { "type": "string", "minLength": 1, "maxLength": 512 },
    "status": { "type": "string", "enum": [ "active", "disabled" ] }
  },
  "required": [ "clientId", "clientSecret" ]
}

Responses

200 — The application state.

FieldTypeRequired
provider"google" | "microsoft"yes
configuredbooleanyes
clientIdstring | nullyes
hasSecretbooleanyes
clientSecretHintstring | nullyes
status"active" | "disabled" | nullyes
redirectUristring (uri)yes
scopesstring[]yes
scopesByCapabilityobjectyes
clientIdPatternstringyes
clientIdHintstringyes
updatedAtstring (date-time) | nullyes

Same schema as GET /api/v1/admin/oauth-apps/{provider}.

400 — The request does not match its schema.

401 — No valid session or API key (auth.unauthenticated).

403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).

429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.

Error codes — oauth.unsupported_provider, oauth.encryption_disabled, request.bad_request, oauth.invalid_client_id. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.

POST /api/v1/oauth/{provider}/start ​

Start an OAuth connection

Returns the authorisation URL to send the browser to. The body only names the capability (mail by default); scopes and redirect come from the instance. Requires a registered application for the provider.

Access — Member session only: API keys are refused (api_key.session_required).

Parameters

NameInTypeRequired
providerpathstringyes

Request body (application/json)

FieldTypeRequired
capability"mail" | "drive" | "calendar" | "sheets" | "files" | "sharepoint"no
JSON Schema
json
{
  "type": "object",
  "properties": {
    "capability": {
      "type": "string",
      "enum": [ "mail", "drive", "calendar", "sheets", "files", "sharepoint" ]
    }
  }
}

Responses

200 — The authorisation URL and its expiry.

FieldTypeRequired
authorizationUrlstring (uri)yes
expiresAtstring (date-time)yes
JSON Schema
json
{
  "type": "object",
  "properties": {
    "authorizationUrl": { "type": "string", "format": "uri" },
    "expiresAt": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
    }
  },
  "required": [ "authorizationUrl", "expiresAt" ],
  "additionalProperties": false
}

400 — The request does not match its schema.

401 — No valid session or API key (auth.unauthenticated).

403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).

429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.

Error codes — oauth.unsupported_provider, request.bad_request, oauth.app_not_configured, oauth.capability_not_supported, oauth.encryption_disabled. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.