Galaxy ID← API docs

GIW POC — Identity Platform custom APIs (0.1.0)

Download OpenAPI specification:

Custom (non-standard) APIs of the GIW POC Identity Platform.

Scope of this document. It specifies the APIs Galaxy builds, in two groups:

  1. Portal API — what the browser calls. The Wi-Fi Portal renders its own UI; there is no Keycloak-rendered login page anywhere in this platform.
  2. Service APIs — HR Verification, Entitlement, Session.

The Portal ↔ Galaxy ID leg is standard OAuth 2.0 / OIDC over the token and admin endpoints (RFC 6749 §4.3 Resource Owner Password Credentials, RFC 7009) and is deliberately not re-specified here — see API-OVERVIEW.md §2. Treating a standard endpoint as a bespoke Galaxy API is how integration teams end up with a private fork of OIDC.

Authentication is POC-only. The portal authenticates over Direct Grant (grant_type=password). RFC 9700 states the password grant MUST NOT be used and no exception is claimed here; it is a constrained POC workaround for captive-portal UX validation. Production authentication architecture remains TBC and should prefer browser-based OIDC/federation. The browser / Authorization Code + PKCE path is retained in the realm as PRODUCTION CANDIDATE — see ../docs/AUTH-STRATEGY.md.

Status. POC. Nothing here is a production contract. Timeouts, TTLs and tier names are POC assumptions, not SLAs — see API-REVIEW-CHECKLIST.md.

Privacy. citizenId (CCCD) appears in exactly one request body in this whole specification, and in no response, no log field and no token claim. See DATA-CLASSIFICATION.md.

Portal Auth

Provider: Wi-Fi Portal. Consumer: the portal's own browser frontend.

These are the only endpoints the browser ever calls. The portal holds the tokens; the browser holds an opaque sid cookie and nothing else. Each successful call returns the whole outcome — identity, entitlement and network session — because a captive portal user has one question ("am I online?") and should not have to make three round trips to answer it.

Sign in an existing Galaxy ID customer

The portal forwards the credentials to Galaxy ID's token endpoint (OAuth 2.0 Resource Owner Password Credentials, confidential client), then obtains an entitlement and opens a network session before answering.

POC ONLY. Direct Grant is a constrained POC workaround; production auth architecture is TBC and should prefer browser OIDC/federation. See ../docs/AUTH-STRATEGY.md.

The portal never verifies a credential itself. It has no password store and no HR access; every authentication decision is Keycloak's.

The password is transited, never persisted and never logged. JavaScript strings are immutable so it cannot be zeroed; it goes out of scope when the request ends. That is the honest guarantee.

On success the response sets an opaque, HttpOnly, SameSite=Strict sid cookie. Tokens stay server-side.

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Request Body schema: application/json
required
username
required
string <email>
password
required
string <password> non-empty

SECRET. Forwarded once to Galaxy ID and discarded. Never stored, logged or echoed by the portal.

Responses

Response Headers
Set-Cookie
string

sid=; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600

X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
authenticated
required
boolean
Value: true
correlationId
required
string
client
string
Enum: "wifi-portal-api" "wifi-portal-employee-api"

Which Galaxy ID client issued the token.

required
object

Verified token claims, for display and support. See TOKEN-CLAIMS.md. Never contains a CCCD.

object (Entitlement)
object (Session)
rawAccessToken
string

Present only when DEBUG_EXPOSE_TOKEN=1. A demo aid for seeding an API client. Must be off outside a laptop.

Request samples

Content type
application/json
{
  • "username": "an.nguyen@example.test",
  • "password": "Passw0rd!23"
}

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "correlationId": "corr-82db1a70-b88d-4543-85b3-dc532b5e9386",
  • "client": "wifi-portal-employee-api",
  • "claims": {
    },
  • "entitlement": {
    },
  • "session": {
    }
}

Create a Galaxy ID and sign straight in

Creates the account through Galaxy ID's Admin API using the portal's own service account (scoped to manage-users, deliberately not realm-admin), then performs an ordinary login.

Register-then-login on purpose: exactly one code path mints a session, so there is one place where credentials are checked and one place where an entitlement is issued.

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Request Body schema: application/json
required
email
required
string <email>
password
required
string <password> >= 8 characters

SECRET. Minimum 8 characters, checked before Galaxy ID is called.

displayName
string

Optional. Split on whitespace into firstName / lastName.

Responses

Response Headers
Set-Cookie
string

sid=; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600

X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
authenticated
required
boolean
Value: true
correlationId
required
string
client
string
Enum: "wifi-portal-api" "wifi-portal-employee-api"

Which Galaxy ID client issued the token.

required
object

Verified token claims, for display and support. See TOKEN-CLAIMS.md. Never contains a CCCD.

object (Entitlement)
object (Session)
rawAccessToken
string

Present only when DEBUG_EXPOSE_TOKEN=1. A demo aid for seeding an API client. Must be off outside a laptop.

Request samples

Content type
application/json
{
  • "email": "an.nguyen@example.test",
  • "password": "Passw0rd!23",
  • "displayName": "Nguyễn Văn An"
}

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "correlationId": "corr-82db1a70-b88d-4543-85b3-dc532b5e9386",
  • "client": "wifi-portal-employee-api",
  • "claims": {
    },
  • "entitlement": {
    },
  • "session": {
    }
}

Verify an employee by Employee ID + Citizen ID

POC ONLY. See ../docs/AUTH-STRATEGY.md. The production candidate for this journey is browser OIDC federating to an Enterprise IdP (ADR-003), which would also remove the CCCD.

The portal forwards both fields to Galaxy ID's token endpoint against a client bound to the custom employee-direct-grant flow. Keycloak then calls the HR Verification API and authenticates only on MATCH + ACTIVE.

The portal is a relay here, not a decision maker: it holds no HR credential and cannot verify anyone.

citizenId is sent as its own field, never as the OAuth password parameter — proxies, APM agents and error reporters treat password specially, and a CCCD must stay away from that machinery.

CCCD handling: transient. Not stored, not logged, not in any token, not echoed in any response. Enforced by automated tests.

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Request Body schema: application/json
required
employeeId
required
string^[A-Za-z0-9._-]{3,32}$
citizenId
required
string^[0-9]{9,12}$

SENSITIVE PERSONAL DATA (Law 91/2025/QH15). Transient verification input only. Must not be stored, logged, cached, echoed, or placed in a token by any party. This is the only place it appears in this API.

Responses

Response Headers
Set-Cookie
string

sid=; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600

X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
authenticated
required
boolean
Value: true
correlationId
required
string
client
string
Enum: "wifi-portal-api" "wifi-portal-employee-api"

Which Galaxy ID client issued the token.

required
object

Verified token claims, for display and support. See TOKEN-CLAIMS.md. Never contains a CCCD.

object (Entitlement)
object (Session)
rawAccessToken
string

Present only when DEBUG_EXPOSE_TOKEN=1. A demo aid for seeding an API client. Must be off outside a laptop.

Request samples

Content type
application/json
{
  • "employeeId": "VJ12345",
  • "citizenId": "001234567890"
}

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "correlationId": "corr-82db1a70-b88d-4543-85b3-dc532b5e9386",
  • "client": "wifi-portal-employee-api",
  • "claims": {
    },
  • "entitlement": {
    },
  • "session": {
    }
}

Read the caller's current state

Always 200. An unauthenticated caller gets {"authenticated": false} rather than a 401, because the entry screen asks this on every page load and an error status there is noise, not information.

The network session is re-read from the Session Service so an expiry or a revocation elsewhere shows up here.

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Schema: application/json
One of
authenticated
required
boolean
Value: true
correlationId
required
string
client
string
Enum: "wifi-portal-api" "wifi-portal-employee-api"

Which Galaxy ID client issued the token.

required
object

Verified token claims, for display and support. See TOKEN-CLAIMS.md. Never contains a CCCD.

object (Entitlement)
object (Session)
rawAccessToken
string

Present only when DEBUG_EXPOSE_TOKEN=1. A demo aid for seeding an API client. Must be off outside a laptop.

Response samples

Content type
application/json
Example
{
  • "authenticated": true,
  • "correlationId": "string",
  • "client": "wifi-portal-api",
  • "claims": { },
  • "entitlement": {
    },
  • "session": {
    },
  • "rawAccessToken": "string"
}

Renew the access token behind the current session

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Headers
Set-Cookie
string

sid=; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600

X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
authenticated
required
boolean
Value: true
correlationId
required
string
client
string
Enum: "wifi-portal-api" "wifi-portal-employee-api"

Which Galaxy ID client issued the token.

required
object

Verified token claims, for display and support. See TOKEN-CLAIMS.md. Never contains a CCCD.

object (Entitlement)
object (Session)
rawAccessToken
string

Present only when DEBUG_EXPOSE_TOKEN=1. A demo aid for seeding an API client. Must be off outside a laptop.

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "correlationId": "corr-82db1a70-b88d-4543-85b3-dc532b5e9386",
  • "client": "wifi-portal-employee-api",
  • "claims": {
    },
  • "entitlement": {
    },
  • "session": {
    }
}

End the session and withdraw network access

Revokes the network session, then performs a back-channel logout against Galaxy ID, then drops the portal session and clears the cookie.

Revoke-first is deliberate: if the IdP call fails, access is already off. Always 200 — a logout that can fail is a logout users do not trust.

Authorizations:
None
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Schema: application/json
ok
boolean
correlationId
string

Response samples

Content type
application/json
{
  • "ok": true,
  • "correlationId": "string"
}

Frontend bootstrap data

Authorizations:
None

Responses

Response Schema: application/json
flight
string
idp
string
mode
string
Value: "api"

Always api. There is no redirect mode.

exposeToken
boolean

Demo aid flag. Must be false outside a laptop.

Response samples

Content type
application/json
{
  • "flight": "VJ178 · SGN→HAN",
  • "idp": "Galaxy ID (Keycloak)",
  • "mode": "api",
  • "exposeToken": true
}

HR Verification

Provider: HR (mocked). Consumer: Keycloak custom authenticator. System of record for employment status.

Verify an employee by Employee ID + Citizen ID

Confirms that the (employeeId, citizenId) pair matches an employee record and reports whether that employee is active and Wi-Fi eligible.

Read-only. No state changes, so a timed-out call is safe to retry. The Keycloak authenticator retries once by default.

No enumeration. "No such employee" and "wrong citizenId" return the byte-identical body {"verified":false,"active":false,"reason":"NOT_FOUND_OR_MISMATCH"}. Comparison is constant-time so response latency does not leak a match either.

Minimal disclosure. The response carries a verification verdict and an opaque employeeRef — never a name, department, salary band or contact detail.

Authorizations:
serviceApiKey
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Request Body schema: application/json
required
employeeId
required
string^[A-Za-z0-9._-]{3,32}$
citizenId
required
string^[0-9]{9,12}$

SENSITIVE PERSONAL DATA (Law 91/2025/QH15). Transient verification input only. Must not be stored, logged, cached, echoed, or placed in a token by any party. This is the only place it appears in this API.

Responses

Response Headers
X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
verified
required
boolean

The id/CCCD pair matched a record

active
required
boolean

That record is an active employee

employeeRef
string

Opaque HR reference. Safe to store; not personally identifying on its own.

company
string
Enum: "VIETJET" "GALAXY" "SOVICO" "PARTNER-X"
wifiEligible
boolean
reason
string
Enum: "NOT_FOUND_OR_MISMATCH" "EMPLOYEE_INACTIVE"

Request samples

Content type
application/json
Example
{
  • "employeeId": "VJ12345",
  • "citizenId": "001234567890"
}

Response samples

Content type
application/json
Example
{
  • "verified": true,
  • "active": true,
  • "employeeRef": "EMP-12345",
  • "company": "VIETJET",
  • "wifiEligible": true
}

Entitlement

Provider: Entitlement Service. Consumer: Wi-Fi Portal. Turns a verified identity into an access decision. Does not enforce.

Decide what access a verified identity is entitled to

Evaluates the caller's verified access token against POC policy and issues an entitlement.

The token is the input, not the body. user_type, employee_verified and company are read from the token after signature, issuer, audience and expiry checks. Body fields are treated as hints: a subjectId that contradicts the token's sub is a 403, and a contradicting userType is logged and ignored. A portal that could assert its own entitlements would make this service decorative.

Idempotent. Repeating a call with the same Idempotency-Key (or the same subject + session) returns the original entitlement with replayed: true rather than minting a second one.

Authorizations:
bearerAuth
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Idempotency-Key
string
Example: 9e115334:corr-9b1f

Repeat-safe key. The same key returns the original result, not a new one.

Request Body schema: application/json
optional
subjectId
string

Must equal the token sub if present, else 403.

userType
string
Enum: "CUSTOMER" "EMPLOYEE"
employeeVerified
boolean
company
string
sessionId
string or null

Responses

Response Schema: application/json
entitlementId
required
string
subjectId
required
string
sessionId
string or null
eligible
required
boolean
entitlementType
required
string
Enum: "WIFI_EMPLOYEE_PACKAGE" "WIFI_CUSTOMER_BASIC"
status
required
string
Enum: "ACTIVE" "DENIED"
validFrom
required
string <date-time>
validUntil
required
string <date-time>
object (Permission)

What the subject may do — deliberately separate from why they may do it (sourceType/sourceRef), per ADR-005.

issuedAt
string <date-time>
replayed
boolean
Response Headers
X-Correlation-ID
string

Echo of the request correlation id

Response Schema: application/json
entitlementId
required
string
subjectId
required
string
sessionId
string or null
eligible
required
boolean
entitlementType
required
string
Enum: "WIFI_EMPLOYEE_PACKAGE" "WIFI_CUSTOMER_BASIC"
status
required
string
Enum: "ACTIVE" "DENIED"
validFrom
required
string <date-time>
validUntil
required
string <date-time>
object (Permission)

What the subject may do — deliberately separate from why they may do it (sourceType/sourceRef), per ADR-005.

issuedAt
string <date-time>
replayed
boolean

Request samples

Content type
application/json
Example
{
  • "subjectId": "9e115334-e225-4a70-a4b7-e785030ce188",
  • "userType": "EMPLOYEE",
  • "employeeVerified": true,
  • "company": "VIETJET",
  • "sessionId": "sess-2f1c"
}

Response samples

Content type
application/json
{
  • "entitlementId": "string",
  • "subjectId": "string",
  • "sessionId": "string",
  • "eligible": true,
  • "entitlementType": "WIFI_EMPLOYEE_PACKAGE",
  • "status": "ACTIVE",
  • "validFrom": "2019-08-24T14:15:22Z",
  • "validUntil": "2019-08-24T14:15:22Z",
  • "permission": {
    },
  • "issuedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Read a previously issued entitlement

Authorizations:
bearerAuth
path Parameters
entitlementId
required
string
Example: ent-6b1e5b70-1f0a-4a1d-9a9e-2f0f1f7c3a10
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Schema: application/json
entitlementId
required
string
subjectId
required
string
sessionId
string or null
eligible
required
boolean
entitlementType
required
string
Enum: "WIFI_EMPLOYEE_PACKAGE" "WIFI_CUSTOMER_BASIC"
status
required
string
Enum: "ACTIVE" "DENIED"
validFrom
required
string <date-time>
validUntil
required
string <date-time>
object (Permission)

What the subject may do — deliberately separate from why they may do it (sourceType/sourceRef), per ADR-005.

issuedAt
string <date-time>
replayed
boolean

Response samples

Content type
application/json
{
  • "entitlementId": "string",
  • "subjectId": "string",
  • "sessionId": "string",
  • "eligible": true,
  • "entitlementType": "WIFI_EMPLOYEE_PACKAGE",
  • "status": "ACTIVE",
  • "validFrom": "2019-08-24T14:15:22Z",
  • "validUntil": "2019-08-24T14:15:22Z",
  • "permission": {
    },
  • "issuedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Session

Provider: Session / Network Access Service. Consumer: Wi-Fi Portal. Enforces a decision that Entitlement already made. Never decides.

Open a network session for a subject

Creates a session in state CREATED. Creation grants nothing: a session without an entitlement carries no access.

deviceRef is an opaque portal-issued handle. MAC addresses are not accepted: iOS 14+ and Android 15 randomise them per network, so a MAC is not a stable device identity and must never anchor metering.

Authorizations:
serviceApiKey
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Idempotency-Key
string
Example: 9e115334:corr-9b1f

Repeat-safe key. The same key returns the original result, not a new one.

Request Body schema: application/json
required
subjectId
required
string
deviceRef
string or null

Opaque portal handle. Never a MAC address.

Responses

Response Schema: application/json
sessionId
required
string
subjectId
required
string
deviceRef
string or null
state
required
string
Enum: "CREATED" "GRANTED" "REVOKED" "EXPIRED"
entitlementId
string or null
entitlementType
string or null
tier
string or null
qosClass
string or null
grantedAt
string or null <date-time>
expiresAt
string or null <date-time>
revokedAt
string or null <date-time>
createdAt
required
string <date-time>
updatedAt
required
string <date-time>
replayed
boolean
Response Schema: application/json
sessionId
required
string
subjectId
required
string
deviceRef
string or null
state
required
string
Enum: "CREATED" "GRANTED" "REVOKED" "EXPIRED"
entitlementId
string or null
entitlementType
string or null
tier
string or null
qosClass
string or null
grantedAt
string or null <date-time>
expiresAt
string or null <date-time>
revokedAt
string or null <date-time>
createdAt
required
string <date-time>
updatedAt
required
string <date-time>
replayed
boolean

Request samples

Content type
application/json
{
  • "subjectId": "9e115334-e225-4a70-a4b7-e785030ce188",
  • "deviceRef": "poc-9e115334"
}

Response samples

Content type
application/json
{
  • "sessionId": "string",
  • "subjectId": "string",
  • "deviceRef": "string",
  • "state": "CREATED",
  • "entitlementId": "string",
  • "entitlementType": "string",
  • "tier": "string",
  • "qosClass": "string",
  • "grantedAt": "2019-08-24T14:15:22Z",
  • "expiresAt": "2019-08-24T14:15:22Z",
  • "revokedAt": "2019-08-24T14:15:22Z",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Read the current state of a session

Authorizations:
serviceApiKey
path Parameters
sessionId
required
string^[A-Za-z0-9-]{1,64}$
Example: sess-2f1c8a90-4b7d-4e01-9c33-8a5f1d2e7b60
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Schema: application/json
sessionId
required
string
subjectId
required
string
deviceRef
string or null
state
required
string
Enum: "CREATED" "GRANTED" "REVOKED" "EXPIRED"
entitlementId
string or null
entitlementType
string or null
tier
string or null
qosClass
string or null
grantedAt
string or null <date-time>
expiresAt
string or null <date-time>
revokedAt
string or null <date-time>
createdAt
required
string <date-time>
updatedAt
required
string <date-time>
replayed
boolean

Response samples

Content type
application/json
{
  • "sessionId": "string",
  • "subjectId": "string",
  • "deviceRef": "string",
  • "state": "CREATED",
  • "entitlementId": "string",
  • "entitlementType": "string",
  • "tier": "string",
  • "qosClass": "string",
  • "grantedAt": "2019-08-24T14:15:22Z",
  • "expiresAt": "2019-08-24T14:15:22Z",
  • "revokedAt": "2019-08-24T14:15:22Z",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Apply an entitlement to a session (open access)

Declarative and idempotent, matching the applyAccess verb in ADR-007: re-applying the same entitlementId is a no-op returning replayed: true.

Refuses with 422 if no entitlementId is supplied. This service never invents access: every grant traces to an entitlement (ADR-008).

Authorizations:
serviceApiKey
path Parameters
sessionId
required
string^[A-Za-z0-9-]{1,64}$
Example: sess-2f1c8a90-4b7d-4e01-9c33-8a5f1d2e7b60
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Request Body schema: application/json
required
entitlementId
required
string
entitlementType
string
tier
string
Enum: "BASIC" "BOOST"
qosClass
string
Enum: "STANDARD" "HIGH"
validUntil
string <date-time>

Responses

Response Schema: application/json
sessionId
required
string
subjectId
required
string
deviceRef
string or null
state
required
string
Enum: "CREATED" "GRANTED" "REVOKED" "EXPIRED"
entitlementId
string or null
entitlementType
string or null
tier
string or null
qosClass
string or null
grantedAt
string or null <date-time>
expiresAt
string or null <date-time>
revokedAt
string or null <date-time>
createdAt
required
string <date-time>
updatedAt
required
string <date-time>
replayed
boolean

Request samples

Content type
application/json
{
  • "entitlementId": "ent-6b1e5b70-1f0a-4a1d-9a9e-2f0f1f7c3a10",
  • "entitlementType": "WIFI_EMPLOYEE_PACKAGE",
  • "tier": "BOOST",
  • "qosClass": "HIGH",
  • "validUntil": "2026-09-21T08:30:00.000Z"
}

Response samples

Content type
application/json
{
  • "sessionId": "string",
  • "subjectId": "string",
  • "deviceRef": "string",
  • "state": "CREATED",
  • "entitlementId": "string",
  • "entitlementType": "string",
  • "tier": "string",
  • "qosClass": "string",
  • "grantedAt": "2019-08-24T14:15:22Z",
  • "expiresAt": "2019-08-24T14:15:22Z",
  • "revokedAt": "2019-08-24T14:15:22Z",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Withdraw access from a session

Idempotent (revokeAccess in ADR-007). Revoking twice is a no-op.

Authorizations:
serviceApiKey
path Parameters
sessionId
required
string^[A-Za-z0-9-]{1,64}$
Example: sess-2f1c8a90-4b7d-4e01-9c33-8a5f1d2e7b60
header Parameters
X-Correlation-ID
string
Example: corr-9b1f2c7e-1a2b-4c3d-8e9f-0a1b2c3d4e5f

Trace id propagated across Identity → HR → Entitlement → Session. Generated if absent and always echoed on the response. Never derived from a CCCD.

Responses

Response Schema: application/json
sessionId
required
string
subjectId
required
string
deviceRef
string or null
state
required
string
Enum: "CREATED" "GRANTED" "REVOKED" "EXPIRED"
entitlementId
string or null
entitlementType
string or null
tier
string or null
qosClass
string or null
grantedAt
string or null <date-time>
expiresAt
string or null <date-time>
revokedAt
string or null <date-time>
createdAt
required
string <date-time>
updatedAt
required
string <date-time>
replayed
boolean

Response samples

Content type
application/json
{
  • "sessionId": "string",
  • "subjectId": "string",
  • "deviceRef": "string",
  • "state": "CREATED",
  • "entitlementId": "string",
  • "entitlementType": "string",
  • "tier": "string",
  • "qosClass": "string",
  • "grantedAt": "2019-08-24T14:15:22Z",
  • "expiresAt": "2019-08-24T14:15:22Z",
  • "revokedAt": "2019-08-24T14:15:22Z",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "replayed": true
}

Member Company Integration

Provider: giw-admin, a Keycloak custom module (RealmResourceProvider) served by Keycloak itself. Consumers: a platform operator through the module's own GUI, and each member company's HR system through the webhook.

MVP POC scope. Connector config, batch import, the inbound webhook and the decision-table editor sit in one module so the whole integration story can be shown in one place. ADR-012 records the target architecture — these concerns split into a separate service — and what keeping them here costs today: realm attributes have no versioning or review, the webhook secret lives beside the config rather than in a vault, and both the event log and the duplicate-suppression window are in memory and lost on restart.

Authentication on /api/* is a realm bearer token carrying realm-management/manage-users. A master-realm admin token is rejected: the module authenticates against the realm it serves. The GUI itself logs in with Authorization Code + PKCE — an admin console has none of the captive-portal constraints that forced Direct Grant on the portal.

List member company connectors

Returns every configured member company. The webhook secret is never included — only hasSecret, saying whether one is set.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

Responses

Response Schema: application/json
Array of objects (Company)

Response samples

Content type
application/json
{}

Create or replace one company's connector settings

Full replace, not a merge. secret is write-only: supplying it stores a new webhook signing secret, omitting it leaves the current one in place, and no response anywhere returns it.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

code
required
string^[A-Z0-9-]{2,24}$
Example: VIETJET

Member company code. Uppercase A–Z, digits and hyphen, 2–24 characters.

Request Body schema: application/json
required
name
required
string
enabled
boolean
Default: false
hrEndpoint
string
loyaltyEndpoint
string
timeoutMs
integer [ 500 .. 15000 ]
Default: 3000
employeeIdPrefix
string
secret
string

Webhook signing secret. Write-only — omit to keep the current one. No response in this API returns it.

Responses

Response Schema: application/json
saved
boolean
code
string

Request samples

Content type
application/json
{
  • "name": "Vietjet Air",
  • "enabled": false,
  • "hrEndpoint": "string",
  • "loyaltyEndpoint": "string",
  • "timeoutMs": 3000,
  • "employeeIdPrefix": "string",
  • "secret": "string"
}

Response samples

Content type
application/json
{
  • "saved": true,
  • "code": "VIETJET"
}

Remove one company's connector settings

Removes configuration and secret. Users already imported from that company are not touched — deleting a connector is not a way to delete people.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

code
required
string^[A-Z0-9-]{2,24}$
Example: VIETJET

Member company code. Uppercase A–Z, digits and hyphen, 2–24 characters.

Responses

Response Schema: application/json
removed
boolean
code
string

Response samples

Content type
application/json
{
  • "removed": true,
  • "code": "VIETJET"
}

Probe the configured HR and Loyalty endpoints

GET {endpoint-origin}/health against each configured endpoint, using that company's timeout. Read-only; nothing is stored. This is how a misconfigured connector is found before an employee hits it at login.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

code
required
string^[A-Z0-9-]{2,24}$
Example: VIETJET

Member company code. Uppercase A–Z, digits and hyphen, 2–24 characters.

Responses

Response Schema: application/json
object (Probe)
object (Probe)

Response samples

Content type
application/json
{
  • "hr": {
    },
  • "loyalty": {
    }
}

Batch import users from a member company

Two modes, and the difference matters. preview works out what would happen and writes nothing; commit does it.

A row is matched in this order: CCCD link token → employee_ref → email → create. Order is not arbitrary — a CCCD identifies a person, an email identifies an inbox.

citizenId is converted to HMAC-SHA256(cccd, realm salt) on arrival and the raw value is dropped. It is not stored, not logged and not echoed in the response.

Import never sets employee_verified. A data load is not a verification; only a live HR check at login may set that.

A preview evaluates each row against the realm as it is now, not against earlier rows in the same file. Two rows sharing a CCCD therefore preview as two creates and commit as create + link. The counts differing between the two modes is expected.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

query Parameters
mode
string
Default: "preview"
Enum: "preview" "commit"

preview (default) writes nothing; commit applies the changes.

Request Body schema: application/json
required
company
required
string
required
Array of objects (ImportRow) [ 1 .. 5000 ] items

Responses

Response Schema: application/json
mode
string
Enum: "PREVIEW" "COMMIT"
company
string
total
integer
object
Array of objects (ImportOutcome)

Request samples

Content type
application/json
{
  • "company": "VIETJET",
  • "rows": [
    ]
}

Response samples

Content type
application/json
{
  • "mode": "PREVIEW",
  • "company": "string",
  • "total": 0,
  • "counts": {
    },
  • "outcomes": [
    ]
}

Receive a change pushed by a member company

Authenticated by HMAC over the raw body, not by a bearer token — the caller is a member company's HR system, not an operator.

X-GIW-Signature is base64(HMAC-SHA256(secret, "{timestamp}.{rawBody}")). Signing the bytes as received is the point: re-serialising the parsed object first would verify our own serialiser, not the sender.

Only events that change who can log in are applied. Events about what someone gets — a loyalty tier, a package purchase — are recorded and not applied, because writing a tier into Keycloak would make it a cache of the loyalty system, and a stale one.

POC limitation. Duplicate suppression and the event log are in memory. An event arriving while Keycloak restarts is lost, and the provider will retry a few times and give up — an employee whose departure nobody recorded. See ADR-012, T-WEBHOOK-DURABILITY.

Authorizations:
None
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

company
required
string
Example: VIETJET
header Parameters
X-GIW-Timestamp
required
string
Example: 1790071678

Unix seconds. Rejected outside ±5 minutes of server time.

X-GIW-Signature
required
string

base64(HMAC-SHA256(secret, "{timestamp}.{rawBody}"))

X-GIW-Event-Id
string <uuid>

Duplicate suppression. Providers retry; without this, applying an event twice is how a reinstated employee ends up disabled again.

Request Body schema: application/json
required
type
required
string

Applied: EMPLOYEE_TERMINATED, EMPLOYEE_SUSPENDED, EMPLOYEE_REINSTATED, EMPLOYEE_COMPANY_CHANGED. Any other type is recorded and not applied.

employeeRef
string

Required for an identity event.

company
string

Target company, for EMPLOYEE_COMPANY_CHANGED.

Responses

Response Schema: application/json
result
string
Enum: "APPLIED" "RECORDED" "DUPLICATE" "NO_MATCH" "CONFLICT" "REJECTED"
detail
string

Request samples

Content type
application/json
{
  • "type": "EMPLOYEE_TERMINATED",
  • "employeeRef": "EMP-12345",
  • "company": "GALAXY"
}

Response samples

Content type
application/json
{
  • "result": "APPLIED",
  • "detail": "string"
}

Recent inbound events and what was done with them

A bounded in-memory ring, most recent first. Enough to show an operator that events are arriving; not a durable event store.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

query Parameters
limit
integer <= 200
Default: 50

Responses

Response Schema: application/json
Array of objects (EventLogEntry)

Response samples

Content type
application/json
{
  • "events": [
    ]
}

Decision models currently loaded

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

Responses

Response Schema: application/json
models
Array of objects
loadedAt
string <date-time>

Response samples

Content type
application/json
{
  • "models": [
    ],
  • "loadedAt": "2019-08-24T14:15:22Z"
}

Read one decision model's DMN source

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

name
required
string
Example: WifiEntitlement

DMN model name as the engine reports it, not the filename.

Responses

Response Schema: application/json
model
string
file
string
xml
string

Response samples

Content type
application/json
{
  • "model": "WifiEntitlement",
  • "file": "wifi-entitlement.dmn",
  • "xml": "string"
}

Replace a decision model and reload it

The old bytes are kept. If the submitted document does not compile they are put back and the previous runtime restored before the caller is told what went wrong — a mistyped FEEL expression must not leave a service that denies everyone.

No approval workflow. Anyone holding manage-users can change who gets access to what, with no version history and no reviewer. This is the same gap already recorded for POST /api/v1/decisions/reload; see DMN-DECISION-SPEC §7.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

name
required
string
Example: WifiEntitlement

DMN model name as the engine reports it, not the filename.

Request Body schema: application/json
required
xml
required
string

The complete DMN 1.3 document

Responses

Response Schema: application/json
saved
boolean
model
string
file
string
reloadedAt
string <date-time>

Request samples

Content type
application/json
{
  • "xml": "string"
}

Response samples

Content type
application/json
{
  • "saved": true,
  • "model": "WifiEntitlement",
  • "file": "wifi-entitlement.dmn",
  • "reloadedAt": "2019-08-24T14:15:22Z"
}

Try a decision without changing anything

For seeing what a rule change would do before saving it. Every input the model declares must be supplied; a missing one is a 422, not a denial. A rule set that cannot evaluate must never read as "denied" — that looks identical to a legitimate refusal.

Authorizations:
bearerAuth
path Parameters
realm
required
string
Default: "galaxy-id-poc"

Keycloak realm the module is serving.

name
required
string
Example: WifiEntitlement

DMN model name as the engine reports it, not the filename.

Request Body schema: application/json
required
required
object
decision
string

One decision by name. Omit to evaluate all of them.

Responses

Response Schema: application/json
model
string
object
correlationId
string

Request samples

Content type
application/json
{
  • "inputs": {
    },
  • "decision": "string"
}

Response samples

Content type
application/json
{
  • "model": "string",
  • "results": { },
  • "correlationId": "string"
}

Operations

Health and test-harness endpoints.

Liveness / readiness probe

Present on mock-hr-api, entitlement-service, session-service and wifi-portal.

Authorizations:
None

Responses

Response Schema: application/json
status
required
string
Value: "UP"
service
required
string

Response samples

Content type
application/json
{
  • "status": "UP",
  • "service": "entitlement-service"
}

TEST HARNESS ONLY — inject an HR failure mode

Forces mock-hr-api to time out or return 503, so the failure cases in section 12 of the task order can be exercised end to end.

Never ship this endpoint shape. It exists because a POC that cannot demonstrate its own failure modes has not been tested.

Authorizations:
None
Request Body schema: application/json
required
mode
string
Enum: "none" "timeout" "unavailable"
delayMs
integer

Responses

Response Schema: application/json
ok
boolean
object

Request samples

Content type
application/json
{
  • "mode": "none",
  • "delayMs": 4000
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "fault": {
    }
}