DotID external developer API example¶
This example shows how a third-party developer integrates an application with DotID through the APaaS public API gateway.
The example application is Acme Vision, a fictional widget camera analytics product. Acme Vision wants to:
register itself as a third-party APaaS application;
expose app-specific scopes;
request OIDC browser login through PassPort;
publish IAM actions and managed permission sets;
publish quota definitions;
publish audit event taxonomy;
become installable by an Account or Organization administrator;
validate a PassPort launch ticket when a user enters the app.
This example documents the clean external developer API model. It
intentionally does not teach legacy registration endpoints, client-registry,
or raw Keycloak Admin APIs.
Runnable code¶
The runnable example lives at:
examples/external-developer-api/
Run it against the local API gateway simulation:
cd examples/external-developer-api
../../scripts/seed-local-example-fixtures.sh
node src/dotid-developer-api.mjs smoke
The smoke command runs the local fixture seed automatically when
FGAI_API_BASE points at localhost or *.dev.fgai.test. The seed creates the
deterministic account, organization, and installer group used by the account and
organization install examples:
DOTID_ACCOUNT_ID=11111111-1111-1111-1111-111111111111
DOTID_ORG_ID=22222222-2222-2222-2222-222222222222
DOTID_GROUP_ID=33333333-3333-3333-3333-333333333333
DOTID_ORG_ACCOUNT_IDS=11111111-1111-1111-1111-111111111111
The example uses the clean public capability API names:
/developer/v1/...
/service-catalog/v1/...
/accounts/v1/...
/organizations/v1/...
/passport/v1/...
Architecture¶
External developers never call Keycloak Admin APIs and never write DotID subsystem tables directly.
The public flow is:
Developer tool
-> public API gateway
-> developer API
-> platform review
-> service catalog publication
-> tenant installation
-> scoped provisioning
-> PassPort app entry
-> third-party application
The rejected flow is:
Developer tool
-> Keycloak Admin API
-> tenant realm mutation
Keycloak is an implementation dependency owned by the platform. Developers describe the integration they need; the platform approves, provisions, audits, and reconciles the resulting state.
Hostnames¶
Environment |
Base URL |
|---|---|
Local simulation |
|
Production |
|
Console ingress is separate from the API surface:
tenant console:
{{ dotid_console_local }}platform admin console:
{{ dotid_admin_console_local }}
Use the console hostnames for browser-console testing. Use the API base URL for external developer API testing.
Actor model¶
This example uses four actors.
Actor |
Description |
|---|---|
Developer |
Builds and registers Acme Vision. Uses developer API credentials. |
Platform reviewer |
Platform admin who approves, rejects, suspends, or retires registrations. |
Tenant admin |
Account or Organization administrator who installs Acme Vision. |
Application user |
User who enters Acme Vision through a PassPort launch link. |
Authentication model¶
Developer APIs use platform-issued developer credentials, not Keycloak Admin tokens.
The API contract assumes a bearer token with claims equivalent to:
{
"iss": "<identity-issuer-url>",
"sub": "developer/acme",
"aud": "developer-api",
"developer_id": "acme",
"scope": "developer.services:write developer.services:read"
}
For production, the issuer is {{ fgai_identity_prod }}.
Lifecycle¶
The service-registration lifecycle is:
DRAFT
-> SUBMITTED
-> APPROVED
-> PUBLISHED
-> DEPRECATED
-> DISABLED
Meaning:
State |
Meaning |
|---|---|
|
Developer can edit the registration. Not visible to tenants. |
|
Frozen for platform review. Not visible to tenants. |
|
Platform has approved the version. Not yet generally installable. |
|
Visible in the service catalog and installable. |
|
Existing installs continue; new installs are blocked or discouraged. |
|
Runtime entry and new provisioning are blocked. |
Tenant installation is a separate lifecycle. Publishing a service does not install it into every Account or Organization.
Target API surface¶
Developer-facing APIs¶
Developer-facing APIs live under:
/developer/v1
Initial target endpoints:
Method |
Path |
Purpose |
|---|---|---|
|
|
Create the service registration manifest; the current facade may publish it immediately after accepting it. |
|
|
List services owned by the developer. |
|
|
Fetch the latest service registration. |
|
|
Idempotent compatibility endpoint for clients that model a separate submit step. |
|
|
Fetch derived review state; reviewer notes remain empty until the review state model exists. |
|
|
Retire the service registration when no active tenant installations remain. |
The current implementation publishes an accepted registration directly as
ACTIVE. Treat submit and review as compatibility/read-side helpers, not
evidence of a separate public review workflow.
Platform review of a submitted registration happens internally, before the service becomes installable. It is not an external developer API call — developer clients never drive the review step.
Tenant installation APIs¶
Tenant installation APIs are exposed to Account and Organization admins:
/accounts/v1/{account_id}/service-installations
/organizations/v1/{org_id}/service-installations
Initial target endpoints:
Method |
Path |
Purpose |
|---|---|---|
|
|
List published services installable by this actor. |
|
|
Inspect permissions, scopes, quotas, and terms. |
|
|
Install for a standalone Account. |
|
|
List active service installations for a standalone Account. |
|
|
Install for an Organization. |
|
|
List active service installations for an Organization. |
|
|
Poll install/provisioning status. |
|
|
Uninstall and deprovision scoped artifacts. |
PassPort runtime APIs¶
PassPort runtime APIs live under:
/passport/v1
Current implemented endpoints include:
Method |
Path |
Purpose |
|---|---|---|
|
|
Fetch registered application metadata. |
|
|
Create a launch link. |
|
|
Redeem a launch token after authentication. |
|
|
Validate an app-bound launch ticket. |
The developer registration API creates or updates the PassPort application
record indirectly. Developers should not write PassPort tables directly.
Smoke tests for GET /passport/v1/applications/{app_id} must provide an app id
that exists in the target environment.
Example service manifest¶
Acme Vision submits one service registration manifest.
{
"service_id": "acme-vision",
"display_name": "Acme Vision",
"description": "Computer vision analytics for widget cameras.",
"publisher": {
"developer_id": "acme",
"display_name": "Acme Robotics, Inc.",
"support_url": "https://acme.example/support",
"terms_url": "https://acme.example/legal/terms",
"privacy_url": "https://acme.example/legal/privacy"
},
"version": "1.0.0",
"surfaces": {
"passport": {
"applications": [
{
"app_id": "acme-vision",
"display_name": "Acme Vision",
"homepage_url": "https://app.acme.example/",
"allowed_redirect_urls": [
"https://app.acme.example/",
"https://app.acme.example/passport/callback"
],
"requested_scopes": [
"acme-vision:Camera:View",
"acme-vision:Frame:Analyze"
],
"supported_launch_contexts": ["account", "organization"],
"eligible_default_app": false
}
]
},
"oidc": {
"clients": [
{
"client_id": "acme-vision-web",
"display_name": "Acme Vision Web",
"application_type": "browser",
"grant_types": ["authorization_code"],
"pkce_required": true,
"redirect_uris": [
"https://app.acme.example/passport/callback"
],
"post_logout_redirect_uris": [
"https://app.acme.example/"
],
"realm_targets": ["account", "identity-center"]
}
]
},
"iam": {
"namespace": "acme-vision",
"actions": [
{
"name": "CameraView",
"action": "acme-vision:Camera:View",
"description": "View camera streams and metadata."
},
{
"name": "FrameAnalyze",
"action": "acme-vision:Frame:Analyze",
"description": "Submit frames for analysis."
}
],
"managed_policies": [
{
"name": "AcmeVisionReadOnly",
"description": "Read camera streams and analysis results.",
"statements": [
{
"Effect": "Allow",
"Action": ["acme-vision:Camera:View"],
"Resource": "*"
}
]
},
{
"name": "AcmeVisionOperator",
"description": "Use Acme Vision analysis features.",
"statements": [
{
"Effect": "Allow",
"Action": [
"acme-vision:Camera:View",
"acme-vision:Frame:Analyze"
],
"Resource": "*"
}
]
}
],
"managed_permission_sets": [
{
"name": "AcmeVisionViewer",
"policy_names": ["AcmeVisionReadOnly"]
},
{
"name": "AcmeVisionOperator",
"policy_names": ["AcmeVisionOperator"]
}
]
},
"quota": {
"definitions": [
{
"quota_key": "camera-streams",
"display_name": "Camera streams",
"unit": "streams",
"default_limit": 50,
"adjustable": true,
"scope": "account"
},
{
"quota_key": "frame-analysis-per-minute",
"display_name": "Frame analysis requests per minute",
"unit": "requests/minute",
"default_limit": 120,
"adjustable": true,
"scope": "account"
}
]
},
"audit": {
"events": [
{
"event_name": "acme-vision.CameraLinked",
"category": "configuration",
"severity": "medium",
"resource_type": "camera",
"payload_schema_ref": "https://schemas.acme.example/audit/camera-linked.v1.json"
},
{
"event_name": "acme-vision.FrameAnalyzed",
"category": "data",
"severity": "low",
"resource_type": "frame",
"payload_schema_ref": "https://schemas.acme.example/audit/frame-analyzed.v1.json"
}
]
},
"backend": {
"openapi_url": "https://api.acme.example/openapi.json",
"health_url": "https://api.acme.example/health",
"webhook_url": "https://api.acme.example/flexgalaxy/webhooks",
"data_classification": "tenant-confidential"
}
}
}
Naming rules¶
Use stable, lowercase IDs for machine identifiers:
Field |
Example |
Rule |
|---|---|---|
|
|
Lowercase slug, globally unique in the catalog. |
|
|
Lowercase slug, usually matches service ID for one-app services. |
|
|
Lowercase slug, no tenant/account suffix supplied by developer. |
IAM namespace |
|
Same namespace used in all actions. |
IAM action |
|
Service namespace plus domain action. |
Audit event |
|
Service namespace plus event name. |
Quota key |
|
Stable slug scoped under service ID by DotID. |
The platform owns tenant-specific suffixing, realm placement, generated client IDs, installation IDs, and provisioned resource identifiers.
Walkthrough¶
Set a base URL:
export FGAI_API_BASE={{ dotid_api_local }}
export DEVELOPER_TOKEN="<developer bearer token>"
For production, point FGAI_API_BASE at {{ dotid_api_prod }} instead.
1. Create a service registration¶
curl -fsS \
-X POST "$FGAI_API_BASE/developer/v1/services" \
-H "Authorization: Bearer $DEVELOPER_TOKEN" \
-H "Content-Type: application/json" \
-d @acme-vision.service.json
Expected response:
{
"service_id": "acme-vision",
"version": "1.0.0",
"state": "ACTIVE",
"manifest_id": "srvreg_01jz0acmevision",
"links": {
"self": "/developer/v1/services/acme-vision",
"review": "/developer/v1/services/acme-vision/review",
"catalog": "/service-catalog/v1/services/acme-vision"
}
}
2. Submit compatibility check¶
curl -fsS \
-X POST "$FGAI_API_BASE/developer/v1/services/acme-vision/submit" \
-H "Authorization: Bearer $DEVELOPER_TOKEN" \
-H "Content-Type: application/json" \
-d @acme-vision.service.json
Expected response:
{
"service_id": "acme-vision",
"version": "1.0.0",
"state": "ACTIVE",
"idempotent": true,
"manifest_id": "srvreg_01jz0acmevision"
}
The initial implementation uses the service-registration facade directly. The
manifest is accepted idempotently by (service_id, version) and published when
the facade reaches ACTIVE. submit is retained as an idempotent compatibility
call for clients that already model a separate submission step. A richer
draft/review state model can be added later without changing the public prefix.
3. Retire a service registration¶
After all tenant installations are removed, a developer can retire the service registration so it leaves the public catalog:
curl -fsS \
-X DELETE "$FGAI_API_BASE/developer/v1/services/acme-vision" \
-H "Authorization: Bearer $DEVELOPER_TOKEN"
Expected response:
{
"service_id": "acme-vision",
"version": "1.0.0",
"state": "RETIRED",
"manifest_id": "srvreg_01jz0acmevision"
}
If active installations still exist, the API returns 409 Conflict. Remove
installations through
DELETE /service-catalog/v1/installations/{installation_id} first.
4. Tenant installs the service¶
Once a published version is available in the catalog, an Account admin reviews the entry:
curl -fsS \
"$FGAI_API_BASE/service-catalog/v1/services/acme-vision" \
-H "Authorization: Bearer $ACCOUNT_ADMIN_TOKEN"
The response shows the requested scopes, clients, quotas, audit events, terms URL, support URL, and data classification.
Install into an Account:
curl -fsS \
-X POST "$FGAI_API_BASE/accounts/v1/$ACCOUNT_ID/service-installations" \
-H "Authorization: Bearer $ACCOUNT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service_id": "acme-vision",
"version": "1.0.0",
"grant_permission_sets": ["AcmeVisionViewer"],
"accepted_terms": true
}'
Expected response:
{
"installation_id": "svcinst_01jz0acmevision",
"service_id": "acme-vision",
"version": "1.0.0",
"state": "PROVISIONING",
"provisioning": {
"iam": "PENDING",
"oidc": "PENDING",
"quota": "PENDING",
"audit": "PENDING",
"passport": "PENDING"
}
}
Poll status:
curl -fsS \
"$FGAI_API_BASE/service-catalog/v1/installations/svcinst_01jz0acmevision" \
-H "Authorization: Bearer $ACCOUNT_ADMIN_TOKEN"
Completion:
{
"installation_id": "svcinst_01jz0acmevision",
"service_id": "acme-vision",
"version": "1.0.0",
"state": "ACTIVE",
"provisioning": {
"iam": "READY",
"oidc": "READY",
"quota": "READY",
"audit": "READY",
"passport": "READY"
}
}
5. Create a PassPort launch link¶
After installation, a user can enter Acme Vision through PassPort.
Current PassPort runtime endpoint:
curl -fsS \
-X POST "$FGAI_API_BASE/passport/v1/launch-links" \
-H "Authorization: Bearer $ACCOUNT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"app_id": "acme-vision",
"account_id": "'"$ACCOUNT_ID"'",
"redirect_url": "https://app.acme.example/",
"requested_scopes": ["acme-vision:Camera:View"],
"expires_in_seconds": 900
}'
Expected response:
The launch URL is issued by PassPort, for example under
{{ fgai_passport_prod }} in production.
{
"launch_url": "<passport-launch-url>",
"expires_at": "2026-06-07T12:15:00Z"
}
Local simulation may use a local PassPort host during browser testing:
{{ dotid_passport_local }}
6. Redeem launch and redirect to application¶
The browser follows the PassPort launch URL. PassPort authenticates the user if needed, validates installation and scope, then issues an app-bound launch ticket and redirects only to an allowlisted Acme Vision URL.
Conceptual redirect:
https://app.acme.example/?launch_ticket=ticket_opaque
The launch ticket is short-lived and app-bound. It is not a broad console session.
7. Application validates launch ticket¶
Acme Vision validates the launch ticket before creating an app session:
curl -fsS \
-X POST "$FGAI_API_BASE/passport/v1/launch-tickets/validate" \
-H "Content-Type: application/json" \
-d '{
"app_id": "acme-vision",
"launch_ticket": "ticket_opaque"
}'
Expected response:
{
"valid": true,
"app_id": "acme-vision",
"subject": "user_01jz0example",
"account_id": "acc_01jz0example",
"organization_id": null,
"scopes": ["acme-vision:Camera:View"],
"expires_at": "2026-06-07T12:16:00Z"
}
The application must reject the request if:
validis false;app_iddoes not match its own app ID;required scopes are missing;
the ticket has expired;
the account or organization context is not acceptable for the app session.
Security rules¶
No direct Keycloak mutation¶
Third-party developers must not receive Keycloak Admin credentials.
The platform owns:
realm placement;
generated client IDs when tenant-specific suffixing is required;
redirect URI validation;
protocol mappers;
client scopes;
tenant-scoped provisioning;
disabling and cleanup.
OIDC clients are public + PKCE¶
The acme-vision-web client above is a browser app, so DotID provisions it as a
public client: it uses PKCE (S256) and receives no client_secret. Native
(custom-scheme) and loopback redirects likewise require a public client — a
confidential client that declares one is rejected 400, never silently
downgraded to a secret-bearing client. Public clients are stamped with the aud
(flexgalaxy-api) and account_id mappers automatically. See
OIDC client setup for the full client-type and
redirect-trust model.
HTTPS redirect URLs¶
Production registrations must use HTTPS redirect URLs. Local
.test hostnames are allowed only for local simulation fixtures. A production
https:// redirect on a public client is trusted only when its host is proven via
the app’s verified domains (App Links / Universal Links) — see
Redirect URIs.
No wildcard production origins¶
Do not approve broad redirect patterns such as:
https://*.example.com/*
Prefer exact origins and exact callback paths:
https://app.acme.example/passport/callback
App-scoped PassPort sessions¶
PassPort entry must not grant normal console access.
The app receives only an app-bound launch ticket or app-scoped token. The resulting application session must not be accepted by console apps.
Scope intersection¶
Effective launch scopes are the intersection of:
scopes registered by the app;
scopes approved by platform review;
scopes granted by tenant installation;
scopes requested by the launch link;
scopes allowed for the authenticated user.
If any layer removes a scope, the app does not receive it.
Idempotency¶
Registration and installation APIs should support idempotency keys.
Recommended header:
Idempotency-Key: acme-vision-1.0.0-submit
Repeated requests with the same key and same body should return the existing result. Repeated requests with the same key and a different body should return a conflict.
Error model¶
Use structured errors:
{
"error_code": "redirect_uri_not_allowed",
"message": "Redirect URI is not registered for this application.",
"details": {
"field": "surfaces.passport.applications[0].allowed_redirect_urls[1]"
}
}
Recommended HTTP statuses:
Status |
Meaning |
|---|---|
|
Syntax-level request error outside schema validation. |
|
Missing or invalid bearer token. |
|
Authenticated actor lacks permission. |
|
Registration, service, installation, or app not found. |
|
Idempotency, version, or namespace conflict. |
|
Manifest schema or semantic validation failed. |
|
Rate limit exceeded. |
Non-goals¶
This example does not cover:
direct Keycloak Admin API usage;
normal console login;
StartPoint account selection;
platform-admin UI implementation;
production billing or commercial terms.