Dify OpenAPI (/openapi/v1)#
/openapi/v1 is Dify's user-scoped, bearer-authenticated programmatic API β the primary interface for the difyctl CLI and external integrations. It is described internally as "User-scoped programmatic API (bearer auth)" and is distinct from the cookie-authenticated /console/api surface used by the web UI .
The blueprint is registered at api/controllers/openapi/__init__.py with two pieces of middleware attached at startup:
- Anti-framing security (via
attach_anti_framing) - Version gate (via
attach_version_gate) β rejects outdateddifyctlclients before they reach any endpoint
API Design: Resource-Oriented Paths and AIP-136#
As of Dify 1.16.0 (PR #38367), all /openapi/v1 paths follow REST resource-oriented conventions aligned with AIP-136 . Verb-suffix paths were replaced with standard GET resource reads and AIP-136 :action custom methods:
| Old (verb-suffix) | Current (resource-oriented) |
|---|---|
GET /apps/{id}/describe | GET /apps/{id} |
POST /apps/{id}/run | POST /apps/{id}:run |
POST /apps/{id}/tasks/{tid}/stop | POST /apps/{id}/tasks/{tid}:stop |
GET /apps/{id}/export | GET /apps/{id}/dsl |
GET /apps/{id}/check-dependencies | GET /apps/{id}/dependencies:check |
POST /workspaces/{ws}/switch | POST /workspaces/{ws}:switch |
PUT /workspaces/{ws}/members/{mid}/role | PATCH /workspaces/{ws}/members/{mid} |
POST /apps/{id}/files/upload | POST /apps/{id}/files |
This was a hard cutover β no dual-mount or /v2 strategy β because difyctl and the server ship in lockstep .
Endpoint Surface#
Each domain is implemented in its own module under api/controllers/openapi/ :
| Module | Paths | Notes |
|---|---|---|
apps.py | GET /apps, GET /apps/{id} | List and describe apps; GET /apps/{id} requires UUID |
app_run.py | POST /apps/{id}:run, POST /apps/{id}/tasks/{tid}:stop | Mode-agnostic app execution |
app_dsl.py | GET /apps/{id}/dsl, GET /apps/{id}/dependencies:check, POST /workspaces/{ws}/apps/imports, POST /workspaces/{ws}/apps/imports/{iid}:confirm | DSL export/import |
workspaces.py | GET /workspaces, GET /workspaces/{id}, POST /workspaces/{id}:switch, GET /workspaces/{id}/members, POST /workspaces/{id}/members, DELETE /workspaces/{id}/members/{mid}, PATCH /workspaces/{id}/members/{mid} | Workspace discovery + team member management |
account.py | GET /account, session list, token revoke | Account info |
files.py | POST /apps/{id}/files | File upload for app inputs; returns 400 for no file/multiple files/invalid filename/blocked extension, 413 for oversized files, 415 for unsupported file types |
human_input_form.py | GET /apps/{id}/human-input-forms/{tok}, POST β¦/{tok}:submit | Paused workflow human-input forms |
workflow_events.py | GET /apps/{id}/tasks/{tid}/events | SSE stream reconnect |
apps_permitted_external.py | GET /permitted-external-apps, GET /permitted-external-apps/{id} | External-SSO app discovery β EE only |
oauth_device.py | /oauth/device/* | RFC 8628 device-flow OAuth |
oauth_device_sso.py | /oauth/device/sso/* | SSO device-flow β EE only |
_meta.py | GET /_version | Unauthenticated version/edition probe |
index.py | GET /_health | Health check |
Workspace Collaboration and Team Member Management#
The workspace endpoints in workspaces.py are the bearer-authenticated counterpart to the console's cookie-auth workspace APIs. Key behavior:
GET /workspacesβ lists workspaces for the authenticated account; external SSO tokens (noaccount_id) return an empty list .POST /workspaces/{id}:switchβ server-side workspace switch used bydifyctl use workspace <id>. The CLI must abort on failure to preventhosts.ymldiverging from server state .GET /workspaces/{id}/membersβ paginated member list; accessible to any member .POST /workspaces/{id}/membersβ invite by email; requiresowneroradminrole, enforces billing/license limits, returns an activation URL .DELETE /workspaces/{id}/members/{mid}β remove a member; self-removal and owner-removal rejected as400.PATCH /workspaces/{id}/members/{mid}β update role; owner role unassignable here (ownership transfer is console-only) .
All write endpoints require WORKSPACE_WRITE scope and account subject β external SSO subjects have no write access .
Authentication and Subject Model#
All endpoints except /_health and /_version use bearer token authentication. Routes declare their requirements via the @endpoint decorator, which combines authentication, request validation, and response serialization under a subject-based routing system implemented in api/controllers/openapi/auth/ .
Two subjects exist, each with its own token prefix and fixed scopes:
dfoa_β Account subject (SubjectType.ACCOUNT): Full access including workspace write and app build/management operations. CarriesScope.FULL.dfoe_β External SSO subject (SubjectType.EXTERNAL_SSO): Read-only access to permitted apps via/permitted-external-apps; no workspace membership and no write access. CarriesScope.APPS_RUNandScope.APPS_READ_PERMITTED_EXTERNAL.
Scopes are bound to the subject type at mint time and enforced at resolve time via libs/oauth_bearer.py.
PR #37641 formalized this split into two CLI "faces" :
appface (usage):describe,run,get,resumeβ dual-subject capable via anAppReaderabstraction that dispatches toAppsClient(account) orPermittedExternalAppsClient(external SSO) depending on the token.studio-appface (build):export,importβ account-only.
The AppReader dispatch is implemented in cli/src/api/app-reader.ts .
Version Gating (difyctl β Server Compatibility)#
Because the API surface undergoes breaking changes on hard cutovers, a symmetric version gate is enforced on both sides :
Server-side (api/controllers/openapi/_version_gate.py):
- Attached as a
before_app_requesthook on the/openapi/v1blueprint. - Rejects
difyctlclients older than0.2.0-alpha(paired with Dify1.16.0) with HTTP 426 Upgrade Required. - Fails open for non-
difyctlor unparseable User-Agent headers β never blocks non-CLI traffic. - Allowlists
/_versionand/_healthso outdated clients can discover the required upgrade.
Client-side (cli/src/version/enforce.ts):
- Probes
GET /_version(unauthenticated, 2-second timeout) before executing any command. - Rejects servers older than
1.16.0with exit code 6 (ErrorCode.VersionSkew); issues a soft nudge for too-new servers. - Caches the compatibility verdict per-host for 1 hour (positive-only cache; fresh servers clear it immediately).
- Fails open on probe timeout or network errors.
Together these gates convert confusing 404 errors (calling a removed path) into explicit upgrade messages .
Generated Contracts and Type Safety#
The CLI uses generated oRPC/Zod contracts that are committed to the repository under packages/contracts/generated/api/openapi/ . The codegen pipeline produces:
orpc.gen.tsβ typed oRPC client with nested method paths (e.g.,apps.byAppId.runrather thanapps.appIdRun)types.gen.tsβ TypeScript types derived from the OpenAPI schemazod.gen.tsβ Zod validators for runtime request/response validation
The backend contract is defined via the @endpoint decorator in _contract.py, which composes @accepts (request validation), @returns (response serialization), and subject-based auth into a single declaration. Response/query models are backed by _models.py . Generated Markdown documentation lives at api/openapi/markdown/openapi-openapi.md . Any route change requires regenerating these contracts.