Audience: App Users — including App Owner, App Manager, App Member, and M2M (Machine-to-Machine) clients
Base URL:
https://{api-gateway-domain}/{stage}Authentication: All endpoints require a valid Bearer token in the
Authorizationheader (except public auth endpoints).User ID Format: User IDs are Amazon aliases (e.g.,
johndoe,janesmith), NOT email addresses.
Note on Login Tracking: Login events are automatically recorded during the token exchange flow (
POST /auth/exchange-token). There is no need to call a separate login tracking API.
The NBS platform defines the following app-level roles with different permission scopes:
| Role | Description | Key Capabilities |
|---|---|---|
| App Owner | Full data access across the entire app | Access all directories (.public, .private, and all user directories), create delegations for others, batch copy files, view all delegations |
| App Manager | Mid-level access with subordinate visibility | Access own directory + subordinates' directories + .public, file operations within scope |
| App Member | Standard user with personal workspace | Access own directory + .public (read-only), file operations in own directory |
| Team Member (via binding) | Access granted through team binding | Same as App Member — access own directory + .public (read-only). Access is automatic when their team is bound to the app. |
| Team Owner | Manages specific teams within an app | View team members, manage team settings for assigned teams. Assigned by App Admin or Platform Admin. |
| Team Leader (capability) | Org hierarchy team leader | Elevated visibility within their team context. Detected automatically from org hierarchy teamLeader field. Returned as a capability in /permissions endpoint. |
| M2M Client | Machine-to-machine service account | Authenticated via Cognito client credentials, can call external audit API and other authorized endpoints |
Note: App and user management (adding/removing users, updating roles, etc.) is performed by the App Admin role, which is documented in the Platform Admin API Reference. The App Owner role described here focuses on data access privileges within the app.
Users may have an effective role higher than their native role through active delegations:
- A member with a FULL delegation from an owner gets effectiveRole: "owner"
- A member with a READ_ONLY delegation can read (but not write) the grantor's directories
- The effectiveRole is returned in token exchange/verify responses
These endpoints handle the OAuth 2.0 PKCE flow for human users. They are public (no Bearer token required).
Note: The
GET /auth/configendpoint has been removed. OAuth configuration (client_id, authorization_endpoint, scope) is now embedded inconfig.jsondeployed per environment to S3. See Environment Configuration for staging and production values.
Exchange an OAuth authorization code for access, ID, and refresh tokens. This is the primary login endpoint.
POST /auth/exchange-token
Request Body:
{
"code": "authorization_code_from_redirect",
"code_verifier": "pkce_code_verifier_string",
"redirect_uri": "https://your-app.example.com/callback",
"app_id": "my-app",
"app_name": "My Application"
}
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | ✅ | OAuth authorization code from redirect |
code_verifier |
string | ✅ | PKCE code verifier (generated during authorization) |
redirect_uri |
string | ✅ | Must exactly match the redirect URI used in authorization request |
app_id |
string | ✅ | Application ID to check permissions against |
app_name |
string | ❌ | Display name (used for login tracking) |
Response 200 OK:
{
"data": {
"access_token": "eyJhbGciOi...",
"id_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"expires_at": "2026-03-21T09:00:00.000Z",
"user_info": {
"sub": "johndoe",
"email": "johndoe@amazon.com",
"fullname": "John Doe",
"department": "NBS",
"manager": "Jane Smith",
"appId": "my-app",
"userRole": "member",
"effectiveRole": "owner",
"activeDelegations": [
{
"grantorId": "janesmith",
"grantorRole": "owner",
"delegationType": "FULL",
"delegationId": "uuid-..."
}
]
}
}
}
Key Fields in user_info:
- sub — User's Amazon alias (e.g., johndoe)
- userRole — Native role in the app (owner, manager, member, or null for platform apps)
- effectiveRole — Highest role considering active delegations
- activeDelegations — List of active delegations elevating the user's permissions
Access Mode Behavior:
- Whitelist apps: User must have explicit permission → 403 if not authorized
- Public apps: All authenticated users get access with default role member
- Platform apps (app-lobby, app-store): Skip permission check, userRole: null
Error Responses:
- 400 — Missing required parameters
- 401 — Invalid or expired authorization code (INVALID_CODE)
- 403 — User not authorized for whitelist app (ACCESS_DENIED)
Note: Login events are automatically tracked during token exchange.
Refresh an expired access token using a refresh token.
POST /auth/refresh-token
Headers:
Authorization: Bearer <refresh_token>
Request Body:
{
"app_id": "my-app"
}
| Field | Type | Required | Description |
|---|---|---|---|
app_id |
string | ✅ | Application ID for role re-evaluation |
Response 200 OK:
{
"data": {
"access_token": "eyJhbGciOi...",
"id_token": null,
"refresh_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"expires_at": "2026-03-21T09:00:00.000Z",
"user_info": {
"sub": "johndoe",
"fullname": "John Doe"
}
}
}
Important: Amazon Federate does NOT return
id_tokenon refresh — onlyaccess_token+refresh_token. Theuser_infoin the refresh response contains onlysubandfullname(looked up from the organization table). The frontend should useaccess_tokenfor Bearer auth (OAuth 2.0 best practice).
Error Responses:
- 400 — Missing parameters or token decryption error
- 401 — Invalid or expired refresh token (INVALID_REFRESH_TOKEN)
- 403 — User no longer authorized for the app
Verify a token and return trusted identity context from the database. Use this to validate session state.
POST /auth/verify-token
Headers:
Authorization: Bearer <access_token>
Request Body:
{
"app_id": "my-app"
}
Response 200 OK:
{
"data": {
"valid": true,
"user_info": {
"sub": "johndoe",
"appId": "my-app",
"userRole": "member",
"effectiveRole": "owner",
"activeDelegations": [...],
"fullname": "John Doe"
}
}
}
Note: The
user_infoin verify-token response containssub,fullname,appId,userRole,effectiveRole, andactiveDelegations. Identity claims likedepartment, andmanagerare only available in the initial exchange-token response.
Error Responses:
- 401 TOKEN_EXPIRED — Token has expired (client should call refresh-token)
- 401 INVALID_TOKEN — Token signature invalid or tampered (re-login required)
- 403 ACCESS_DENIED — User not in app whitelist (no retry)
- 500 SYSTEM_ERROR — Database/KMS failure
Login Tracking: Login events are automatically recorded during the
POST /auth/exchange-tokenflow. There is no need for app users to call a separate login tracking API.
M2M (Machine-to-Machine) clients authenticate using AWS Cognito Client Credentials flow.
client_id and client_secret via the Cognito token endpointAuthorization: Bearer <cognito_access_token>?auth_source=cognito_m2m query parameter to all API callsclient_id from the Cognito token is used as the user's sub (identity)POST /apps/{appId}/audit/log?auth_source=cognito_m2m
Authorization: Bearer <cognito_access_token>
Content-Type: application/json
{
"operation": "data_export",
"status": "success",
"operationDetails": { "recordCount": 1500 }
}
Important: The
auth_source=cognito_m2mquery parameter tells the API to verify the token against Cognito JWKS instead of Federate JWKS.
M2M clients are identified by their client_id (which becomes their sub). To grant an M2M client access to an app:
1. Register the M2M client's client_id as a user in the app's permissions table
2. Assign appropriate role (owner, manager, or member)
3. The M2M client can then call any API endpoint its role permits
Get all apps accessible by the current authenticated user.
GET /apps
Headers:
Authorization: Bearer <access_token>
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
includeDeleted |
boolean | false |
Include soft-deleted apps |
Behavior by Role:
- Platform Admin: Returns ALL apps across the platform with userRole: "platform_admin"
- Regular User: Returns whitelist apps (where user has explicit permission) + all public apps
Response 200 OK:
{
"data": {
"apps": [
{
"appId": "my-app",
"appName": "My Application",
"description": "A data insights tool",
"status": "active",
"accessMode": "whitelist",
"category": "data-insights",
"userRole": "owner",
"userCount": 15,
"adminCount": 2,
"entryUrl": "https://my-app.example.com",
"iconUrl": "https://presigned-url...",
"createdAt": "2026-01-15T10:00:00.000Z"
},
{
"appId": "public-dashboard",
"appName": "Public Dashboard",
"status": "active",
"accessMode": "public",
"userRole": "member",
"userCount": 0,
"adminCount": 1
}
],
"role": "app_user",
"totalApps": 2
}
}
Note: For platform admins,
roleis"platform_admin"and all apps are returned.
All file operation endpoints require app-level permissions and are scoped to {appId}.
Each app has the following directory structure in S3:
{appId}/
├── .private/ ← Owner-only access (read/write)
├── .public/ ← All members can read; only owners can write
├── {userId}/ ← Personal workspace for each user
│ ├── subfolder/
│ └── document.pdf
├── .teams/{teamId}/ ← Team directory (auto-created when team is bound)
│ ├── .public/ ← Team shared files (members: read; team leader/owner: read+write)
│ └── .private/ ← Team leader and team_owner only (read/write)
└── {anotherUserId}/
| Your Role | .private |
.public |
Own Directory | Other User's Dir | .teams/{id}/.public |
.teams/{id}/.private |
|---|---|---|---|---|---|---|
| Owner | Read ✅ Write ✅ | Read ✅ Write ✅ | Read ✅ Write ✅ | Read ✅ Write ✅ | Read ✅ Write ✅ | Read ✅ Write ✅ |
| Manager | ❌ | Read ✅ | Read ✅ Write ✅ | Subordinates only | ❌ (unless also team member) | ❌ |
| Member | ❌ | Read ✅ | Read ✅ Write ✅ | ❌ | ❌ (unless also team member) | ❌ |
| Team Owner (explicit) | ❌ | Read ✅ | Read ✅ Write ✅ | Managed team members | Read ✅ Write ✅ (managed teams) | Read ✅ Write ✅ (managed teams) |
| Team Member (auto) | ❌ | Read ✅ | Read ✅ Write ✅ | ❌ | Read ✅ (own team only) | ❌ |
| Team Leader (auto) | ❌ | Read ✅ | Read ✅ Write ✅ | ❌ | Read ✅ Write ✅ (led team + descendants) | Read ✅ Write ✅ (led team + descendants) |
| Sub-team Member (recursive) | ❌ | Read ✅ | Read ✅ Write ✅ | ❌ | Own sub-team: Read ✅; Parent team: only if allowChildAccessToDir=true |
❌ |
Important Notes:
- Team Member and Team Leader are NOT explicitly assigned — they are automatically derived from team binding + org hierarchy data.
- Team Owner is an explicit role assigned by App Admin or Platform Admin via
POST /apps/{appId}/teams/{teamId}/owner.- Roles can stack: a user can have both an explicit role (e.g.,
owner,manager) AND be a team member/leader. The explicit personal permission record always takes precedence for role determination.- Sub-team members' access to the parent team's
.teams/{parentId}/.publicis controlled by theallowChildAccessToDirflag set at binding time.
Delegation: If you have an active delegation from another user, you can access their directories based on the delegation type (
FULL= read+write,READ_ONLY= read only).
List files and directories within an app.
GET /apps/{appId}/files
Required Permission: app:files:list
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
directory |
string | — | Target directory (required for members, optional for owners/managers) |
recursive |
boolean | false |
List all files recursively (owner only) |
search |
string | — | Wildcard pattern for filename matching (e.g., *report*.xlsx) |
includeDeleted |
boolean | false |
Include soft-deleted files (owner only) |
continuationToken |
string | — | Pagination token |
maxKeys |
integer | 1000 |
Maximum number of keys to return |
Examples:
# Member: list own files
GET /apps/my-app/files?directory=johndoe
# Member: list files in a subfolder
GET /apps/my-app/files?directory=johndoe/reports
# Member: list public files
GET /apps/my-app/files?directory=.public
# Owner: list all top-level directories
GET /apps/my-app/files
# Owner: recursive search across all directories
GET /apps/my-app/files?recursive=true&search=*quarterly*.xlsx
# Manager: list scoped directories (own + subordinates + .public)
GET /apps/my-app/files
Response 200 OK (with directory specified):
{
"data": {
"files": [
{
"key": "my-app/johndoe/report.pdf",
"filename": "report.pdf",
"size": 1048576,
"lastModified": "2026-03-20T15:30:00.000Z",
"owner": "johndoe"
}
],
"directories": [
{
"directory": "reports",
"prefix": "my-app/johndoe/reports/"
}
],
"totalFiles": 1,
"totalDirectories": 1,
"totalSize": 1048576,
"prefix": "my-app/johndoe/"
}
}
Response 200 OK (Manager scoped list, no directory):
{
"data": {
"directories": [
{ "directory": "johndoe", "prefix": "my-app/johndoe/" },
{ "directory": "subordinate1", "prefix": "my-app/subordinate1/" },
{ "directory": ".public", "prefix": "my-app/.public/" }
],
"totalDirectories": 3,
"prefix": "my-app/"
}
}
Error Responses:
- 400 MISSING_DIRECTORY — Directory parameter required for non-owner users
- 403 ACCESS_DENIED — No access to the requested directory
- 403 PERMISSION_DENIED — Recursive/includeDeleted only for owners
Generate a presigned S3 PUT URL for file upload.
POST /apps/{appId}/files/upload
Required Permission: app:files:upload
Request Body:
{
"directory": "johndoe",
"filename": "quarterly-report.pdf",
"contentType": "application/pdf",
"expires_in": 900
}
| Field | Type | Required | Description |
|---|---|---|---|
directory |
string | ✅ | Target directory (your alias, .public, or subdirectory path) |
filename |
string | ✅ | Filename (no slashes allowed) |
contentType |
string | ✅ | MIME type (e.g., application/pdf, image/png) |
expires_in |
integer | ❌ | URL expiration in seconds (default: 900, max: 3600) |
Response 200 OK:
{
"data": {
"uploadUrl": "https://nbs-data-storage.s3.ap-southeast-2.amazonaws.com/my-app/johndoe/quarterly-report.pdf?X-Amz-...",
"expiresIn": 900,
"requiredHeaders": {
"Content-Type": "application/pdf"
},
"s3Key": "my-app/johndoe/quarterly-report.pdf"
}
}
How to Upload:
# Use the presigned URL with the EXACT Content-Type from requiredHeaders
curl -X PUT \
-H "Content-Type: application/pdf" \
--data-binary @quarterly-report.pdf \
"https://nbs-data-storage.s3.ap-southeast-2.amazonaws.com/my-app/johndoe/quarterly-report.pdf?X-Amz-..."
⚠️ Critical: The
Content-Typeheader in your upload request MUST exactly match thecontentTypeyou specified. Mismatched Content-Type will result in a403 SignatureDoesNotMatcherror.
Error Responses:
- 400 MISSING_CONTENT_TYPE — contentType is required
- 400 INVALID_FILENAME — Filename contains invalid characters
- 403 ACCESS_DENIED — No write access to the directory
Generate a presigned S3 GET URL for file download.
POST /apps/{appId}/files/download
Required Permission: app:files:download
Request Body:
{
"directory": "johndoe",
"filename": "quarterly-report.pdf",
"expires_in": 300
}
| Field | Type | Required | Description |
|---|---|---|---|
directory |
string | ✅ | Source directory (including subdirectory path if needed) |
filename |
string | ✅ | Filename to download (no slashes) |
expires_in |
integer | ❌ | URL expiration in seconds (default: 300, max: 3600) |
Response 200 OK:
{
"data": {
"downloadUrl": "https://nbs-data-storage.s3.ap-southeast-2.amazonaws.com/my-app/johndoe/quarterly-report.pdf?X-Amz-...",
"expiresIn": 300,
"filename": "quarterly-report.pdf"
}
}
Error Responses:
- 403 ACCESS_DENIED — No read access to the directory
- 404 NOT_FOUND — File not found
Delete a file from S3.
DELETE /apps/{appId}/files
Required Permission: app:files:delete
Request Body:
{
"directory": "johndoe",
"filename": "old-report.pdf"
}
| Field | Type | Required | Description |
|---|---|---|---|
directory |
string | ✅ | Directory containing the file |
filename |
string | ✅ | Filename to delete (no slashes) |
Response 200 OK:
{
"data": {
"message": "File deleted successfully",
"deletedKey": "my-app/johndoe/old-report.pdf"
}
}
Error Responses:
- 403 ACCESS_DENIED — No write access to the directory
Copy a single file within the same app using S3 server-side copy.
POST /apps/{appId}/files/copy
Required Permission: app:files:copy
Request Body:
{
"sourceDirectory": ".public/templates",
"sourceFilename": "baseline.xlsx",
"targetDirectory": "johndoe/reports",
"targetFilename": "my-baseline.xlsx"
}
| Field | Type | Required | Description |
|---|---|---|---|
sourceDirectory |
string | ✅ | Source directory (can include subdirectory path) |
sourceFilename |
string | ✅ | Source filename (no slashes) |
targetDirectory |
string | ✅ | Target directory (can include subdirectory path) |
targetFilename |
string | ✅ | Target filename (no slashes) |
Access Control:
- You need read access to the source directory
- You need write access to the target directory
- Members can copy from .public or own directory to own directory
Response 200 OK:
{
"data": {
"sourceKey": "my-app/.public/templates/baseline.xlsx",
"targetKey": "my-app/johndoe/reports/my-baseline.xlsx",
"message": "File copied successfully"
}
}
Batch copy all files from one directory to another. Owner only.
POST /apps/{appId}/files/copy-all
Required Permission: app:files:copy + Owner role
Request Body:
{
"sourceDirectory": "departing-user",
"targetDirectory": ".private/backup-20260321"
}
| Field | Type | Required | Description |
|---|---|---|---|
sourceDirectory |
string | ✅ | Source directory to copy from |
targetDirectory |
string | ✅ | Target directory to copy to |
Response 200 OK:
{
"data": {
"copiedFiles": [
{ "sourceKey": "my-app/departing-user/file1.pdf", "targetKey": "my-app/.private/backup-20260321/file1.pdf" }
],
"failedFiles": [],
"totalCopied": 42
}
}
Error Responses:
- 403 PERMISSION_DENIED — Only app owners can batch copy
Delegations allow users to temporarily grant their file access permissions to other users.
| Type | Permissions Granted | Use Case |
|---|---|---|
| FULL | list, upload, download, delete | Vacation coverage, full handover |
| READ_ONLY | list, download | Audit access, review-only sharing |
effectiveRole is elevated to the grantor's role (for FULL delegations)Delegate your own permissions to another user.
POST /apps/{appId}/delegations/self
Access: Any authenticated app user with a native role
Request Body:
{
"delegateeId": "colleague",
"delegationType": "FULL",
"expiry": "2026-04-01T00:00:00.000Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
delegateeId |
string | ✅ | Amazon alias of the person receiving the delegation |
delegationType |
string | ✅ | FULL or READ_ONLY |
expiry |
string | ❌ | ISO 8601 expiry timestamp (no expiry if omitted) |
Response 201 Created:
{
"data": {
"delegationId": "uuid-...",
"appId": "my-app",
"grantorId": "johndoe",
"delegateeId": "colleague",
"delegationType": "FULL",
"status": "active",
"expiry": "2026-04-01T00:00:00.000Z",
"createdAt": "2026-03-21T08:00:00.000Z",
"createdBy": "johndoe"
}
}
Error Responses:
- 400 — Missing fields, self-delegation (delegateeId = yourself), invalid type
- 403 — No native role, or attempting transitive delegation
- 404 — Delegatee not found or inactive in the app
- 409 — Duplicate active delegation already exists
Get all delegations where you are either the grantor or delegatee.
GET /apps/{appId}/delegations/mine
Access: Any authenticated app user
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
status |
string | active |
active, revoked, or all |
Response 200 OK:
{
"data": {
"delegations": [
{
"delegationId": "uuid-...",
"appId": "my-app",
"grantorId": "johndoe",
"delegateeId": "colleague",
"delegationType": "FULL",
"status": "active",
"expiry": "2026-04-01T00:00:00.000Z",
"createdAt": "2026-03-21T08:00:00.000Z",
"createdBy": "johndoe"
}
]
}
}
Revoke an active delegation. Can be done by the grantor, delegatee, or app owner.
DELETE /apps/{appId}/delegations/{delegationId}
Access: Grantor, delegatee, or App Owner
Response 200 OK:
{
"data": {
"message": "Delegation revoked successfully",
"delegationId": "uuid-..."
}
}
Error Responses:
- 400 — Already revoked
- 403 — Not authorized to revoke (not grantor, delegatee, or owner)
- 404 — Delegation not found
Search for active users within an app (for delegation target selection, etc.).
GET /apps/{appId}/users/search?query=john
Access: Any authenticated app user
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | ✅ | Search prefix for userId or substring for name |
Response 200 OK:
{
"data": {
"users": [
{
"userId": "johndoe",
"alias": "johndoe",
"name": "John Doe",
"email": "johndoe@amazon.com",
"role": "owner"
}
]
}
}
Note: Returns up to 20 matching users. Only active users are returned.
Search the organization directory for users to add to an app.
GET /apps/{appId}/org-users/search?query=john
Access: Users with app:users:manage permission (App Admin)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | ✅ | Search prefix for userId or substring for employeeName |
Response 200 OK:
{
"data": {
"users": [
{ "alias": "johndoe", "name": "John Doe", "status": "active" },
{ "alias": "johnsmith", "name": "John Smith", "status": "active" }
]
}
}
Note: Only non-deleted organization members are returned. Returns up to 20 results.
Look up a specific user's organization context by exact userId. Returns the user's direct manager and all active direct reportees (including each reportee's own reportee count).
GET /org/users?userId={userId}
Access: Any authenticated user (no admin role required)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId |
string | ✅ | Exact Amazon alias to look up (case-sensitive) |
Example:
GET /org/users?userId=johndoe
Response 200 OK:
{
"userId": "johndoe",
"alias": "johndoe",
"name": "John Doe",
"email": "johndoe@amazon.com",
"manager": {
"userId": "janesmith",
"name": "Jane Smith"
},
"reportees": [
{
"userId": "alicejones",
"name": "Alice Jones",
"reporteeCount": 3
},
{
"userId": "boblee",
"name": "Bob Lee",
"reporteeCount": 0
}
],
"activeReporteeCount": 2,
"jobLevelName": "6"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
userId |
string | Amazon alias (same as alias) |
alias |
string | Amazon alias |
name |
string | Full display name from org table |
email |
string | Derived as {userId}@amazon.com |
manager |
object | null | Direct manager's userId and name; null if top of hierarchy |
reportees |
array | All active direct reports, each with userId, name, and reporteeCount |
activeReporteeCount |
integer | Total count of active direct reports |
reportees[].reporteeCount |
integer | Number of active direct reports that this reportee themselves manages |
jobLevelName |
string | null | Job level (e.g., "4", "5", "6"). Only present if synced from RCM parquet. |
Manager Derivation Logic:
The manager is determined from the pre-computed managerId field written at sync time:
- If level_N == userId, then level_{N-1} is the manager
- If the user appears at level_1 (top of hierarchy), manager is null
- If the user does not appear in any level field, level_4 is treated as the manager
Error Responses:
- 400 VALIDATION_ERROR — userId parameter is missing or empty
- 401 — Missing or invalid Bearer token
- 404 NOT_FOUND — User not found in organization table or has status: deleted
This endpoint allows registered apps (both human users and M2M clients) to write user operation logs to the platform's audit system.
POST /apps/{appId}/audit/log
Access: Any authenticated user (Federate) or M2M client (Cognito)
For M2M clients, add ?auth_source=cognito_m2m:
POST /apps/{appId}/audit/log?auth_source=cognito_m2m
Request Body:
{
"operation": "data_export",
"status": "success",
"operationDetails": {
"recordCount": 1500,
"exportFormat": "csv",
"targetBucket": "analytics-output"
},
"filePath": "exports/2026-03/quarterly-report.csv",
"errorMessage": ""
}
| Field | Type | Required | Description |
|---|---|---|---|
operation |
string | ✅ | Operation name (alphanumeric, _, :, ., -, /; max 100 chars) |
status |
string | ❌ | success, failed, error, or partial (default: success) |
operationDetails |
object | ❌ | Free-form JSON metadata (max 4 KB serialized) |
filePath |
string | ❌ | Related file path (max 500 chars, no control characters) |
errorMessage |
string | ❌ | Error description when status ≠ success (max 1000 chars) |
Validation Rules:
- operation must match ^[a-zA-Z0-9_:./-]{1,100}$
- operationDetails serialized JSON must be ≤ 4 KB
- filePath must not contain control characters and ≤ 500 chars
- appId is taken from the URL path (trusted), never from the body
Response 200 OK:
{
"data": {
"message": "Audit log recorded",
"userId": "johndoe",
"appId": "my-app",
"operation": "data_export",
"timestamp": "2026-03-21T08:00:00.000Z"
}
}
Source Tagging:
- Human user (Federate): source = "external_app"
- M2M client (Cognito): source = "external_app_m2m"
Error Responses:
- 400 — Missing operation, invalid characters, or payload too large
- 401 — Unable to determine userId from token
- 404 — App not found in registry
The NBS platform supports team-based access control, allowing apps to bind teams from the organization hierarchy. When a team is bound to an app, all team members automatically receive app access permissions.
Key Concepts:
- Team Binding: Links an org team to an app — members get file access automatically
- Recursive: When
true, sub-team members also get access- Team Owner: A role that can manage team members and view team details within an app
- Team Leader: The designated leader of a team in the org hierarchy (capability returned in user permissions)
Search for teams by name. Used when binding teams to an app.
GET /teams/search?query={query}
Access: Any authenticated user
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | ✅ | Team name search keyword (min 2 characters) |
Response 200 OK:
{
"teams": [
{
"teamId": "12345",
"teamName": "NBS Team",
"parentTeamId": "99999",
"depth": 2
}
]
}
Error Responses:
- 400 — Missing or too short query parameter
- 401 — Missing or invalid Bearer token
List all teams bound to an app. Response detail level varies by role.
GET /apps/{appId}/teams
Access: Any app member (must have a role in the app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
appId |
string | App identifier |
Response 200 OK (privileged user — owner/team_owner/platform admin):
{
"teams": [
{
"teamId": "12345",
"teamName": "NBS Team",
"parentTeamId": "99999",
"depth": 2,
"recursive": true,
"allowChildAccessToDir": false,
"memberCount": 15
}
],
"total": 1
}
Response 200 OK (regular member — limited view):
{
"teams": [
{
"teamId": "12345",
"teamName": "NBS Team",
"parentTeamId": "99999"
}
],
"total": 1
}
Note: Regular members only see basic team info (teamId, teamName, parentTeamId). Privileged users see full binding configuration and member counts.
Error Responses:
- 403 — User is not a member of the app
- 404 — App not found
List members of a specific team bound to an app.
GET /apps/{appId}/teams/{teamId}/members
Access: App Owner, Team Owner (of specified team), or Platform Admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
appId |
string | App identifier |
teamId |
string | Team identifier |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
directOnly |
boolean | false |
Only return direct members (overrides binding's recursive flag) |
Response 200 OK:
{
"teamId": "12345",
"members": [
{
"userId": "johndoe",
"membershipLevel": 0
}
],
"total": 5
}
Note:
membershipLevelindicates the depth from the bound team. Level 0 = direct member, level 1+ = sub-team member (only included when binding hasrecursive: true).
Error Responses:
- 403 — Not authorized (must be app owner, team_owner of this team, or platform admin)
- 404 — App not found
Get all teams a user belongs to within a specific app context.
GET /users/{userId}/teams?appId={appId}
Access: Self (own userId), App Owner, or Team Owner
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
userId |
string | Target user's Amazon alias |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
appId |
string | ✅ | App identifier to check teams for |
Response 200 OK:
{
"userId": "johndoe",
"appId": "my-app",
"teams": [
{
"teamId": "12345",
"teamName": "NBS Team",
"membershipLevel": 0
}
]
}
Error Responses:
- 400 — Missing required appId query parameter
- 403 — Not authorized to view target user's teams
- 404 — App not found
Bind a team to an app, granting all team members automatic access.
POST /apps/{appId}/teams
Access: App Admin or Platform Admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
appId |
string | App identifier |
Request Body:
{
"teamId": "12345",
"recursive": true,
"allowChildAccessToDir": false
}
| Field | Type | Required | Description |
|---|---|---|---|
teamId |
string | ✅ | Team ID to bind (must exist in org hierarchy) |
recursive |
boolean | ✅ | Include sub-team members in access grant |
allowChildAccessToDir |
boolean | ❌ | Allow sub-team members to access team directory (default: false) |
Response 201 Created:
{
"message": "Team bound successfully",
"appId": "my-app",
"teamId": "12345",
"recursive": true,
"allowChildAccessToDir": false
}
Error Responses:
- 400 — Missing required fields or invalid types
- 403 — Not an app admin or platform admin
- 404 — Team not found in org hierarchy
- 409 — Team already bound to this app
Remove a team binding from an app. Team members lose automatic access.
DELETE /apps/{appId}/teams/{teamId}
Access: App Admin or Platform Admin
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
appId |
string | App identifier |
teamId |
string | Team identifier to unbind |
Response 200 OK:
{
"message": "Team unbound successfully",
"appId": "my-app",
"teamId": "12345"
}
Error Responses:
- 403 — Not an app admin or platform admin
- 404 — Team binding not found
Get a user's full permission context including team-based capabilities.
GET /apps/{appId}/users/{userId}/permissions
Access: Any authenticated user (typically used for self-check)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
appId |
string | App identifier |
userId |
string | Target user's Amazon alias |
Response 200 OK:
{
"userId": "johndoe",
"appId": "my-app",
"role": "member",
"accessVia": "team_binding",
"capabilities": [
{
"type": "team_leader",
"teamId": "12345",
"teamName": "NBS Team"
}
]
}
Key Fields:
| Field | Description |
|---|---|
accessVia |
personal_permission (direct role) or team_binding (via team) |
capabilities |
Array of special capabilities. team_leader indicates the user leads a bound team. |
Note: The
team_leadercapability is derived from theteamLeaderfield in the team hierarchy. Team leaders have elevated visibility within their team context.
Error Responses:
- 404 — App or user not found
All error responses follow a consistent format:
{
"statusCode": 403,
"body": {
"error": "Access denied: You can only access your own directory and the .public directory",
"errorCode": "ACCESS_DENIED",
"errorId": "ERR-uuid-..."
}
}
| HTTP Status | Error Code | Description | Action |
|---|---|---|---|
400 |
VALIDATION_ERROR |
Missing or invalid parameters | Fix request parameters |
400 |
MISSING_CONTENT_TYPE |
contentType required for upload | Add contentType to body |
400 |
MISSING_DIRECTORY |
Directory required for non-owner | Add directory parameter |
400 |
INVALID_FILENAME |
Filename contains invalid characters | Fix filename |
401 |
INVALID_TOKEN |
Token expired or invalid | Refresh token or re-login |
401 |
TOKEN_EXPIRED |
Token has expired | Call refresh-token endpoint |
403 |
ACCESS_DENIED |
No access to resource | Check your role and directory access |
403 |
PERMISSION_DENIED |
Feature restricted to higher role | Contact app admin |
404 |
NOT_FOUND |
Resource not found | Verify resource exists |
409 |
CONFLICT |
Resource already exists | Check for duplicates |
500 |
INTERNAL_ERROR |
Server-side error | Retry or contact admin |
Data Access Permissions (applicable to all app-level roles):
| Permission | Owner | Manager | Member | Description |
|---|---|---|---|---|
app:files:list |
✅ | ✅ | ✅ | List files in accessible directories |
app:files:upload |
✅ | ✅ | ✅ | Generate upload presigned URLs |
app:files:download |
✅ | ✅ | ✅ | Generate download presigned URLs |
app:files:delete |
✅ | ✅ | ✅ | Delete files in accessible directories |
app:files:copy |
✅ | ✅ | ✅ | Copy files between accessible directories |
App Admin Permissions (App Admin only — see Platform Admin API Reference for details):
| Permission | Description |
|---|---|
app:admin |
Administrative operations (manage app settings) |
app:users:manage |
Add/remove/update users in the app |
app:config:view |
View app configuration and user list |
| Directory | Owner | Manager | Member |
|---|---|---|---|
.private |
Read ✅ Write ✅ | ❌ | ❌ |
.public |
Read ✅ Write ✅ | Read ✅ Write ❌ | Read ✅ Write ❌ |
| Own directory | Read ✅ Write ✅ | Read ✅ Write ✅ | Read ✅ Write ✅ |
| Subordinate's directory | N/A | Read ✅ Write ✅ | N/A |
| Other user's directory | Read ✅ Write ✅ (active) | ❌ | ❌ |
| Soft-deleted user dir | Read ✅ Write ❌ | ❌ | ❌ |
| Delegation Type | Permissions Granted |
|---|---|
FULL |
app:files:list, app:files:upload, app:files:download, app:files:delete |
READ_ONLY |
app:files:list, app:files:download |
When accessing directories via delegation, the access is based on the grantor's role:
| Grantor Role | Delegatee Can Access |
|---|---|
| Owner | All directories (.private, .public, all user dirs) |
| Manager | Grantor's own directory + grantor's subordinates' directories |
| Member | Grantor's own directory only |
| Access Mode | Who Can Access | How to Add Users |
|---|---|---|
| Whitelist | Only users explicitly added to the app | Add via POST /apps/{appId}/users |
| Public | All authenticated users (auto member role) |
Only owner role can be explicitly added |
M2M clients follow the same permission model as human users:
client_id as a user in the appowner, manager, or member)?auth_source=cognito_m2m on all API callsclient_id serves as the user's identity (sub)Supported M2M Operations:
- External audit logging (POST /apps/{appId}/audit/log)
- File operations (if registered with appropriate role)
- Any endpoint that accepts @require_auth authentication
OAuth configuration is embedded in config.json (deployed per environment to S3). The GET /auth/config endpoint has been removed.
| Field | Staging / Experimental | Production |
|---|---|---|
| API Endpoint | https://nbsaihub.ags.amazon.dev/prod |
https://ai.wwgs.amazon.dev/prod |
client_id |
nbsaihubstaging |
ai-app-store |
authorization_endpoint |
https://idp-integ.federate.amazon.com/api/oauth2/v1/authorize |
https://idp.federate.amazon.com/api/oauth2/v1/authorize |
scope |
openid profile email |
openid profile email |
redirect_uri |
https://nbsaihub.ags.amazon.dev/callback.html |
https://ai.wwgs.amazon.dev/callback.html |
Example config.json for production:
{
"api_gateway_url": "https://ai.wwgs.amazon.dev/prod",
"oauth_config": {
"client_id": "ai-app-store",
"authorization_endpoint": "https://idp.federate.amazon.com/api/oauth2/v1/authorize",
"scope": "openid profile email",
"response_type": "code"
}
}
Pass oauth_config from config.json to the NBSAuth constructor:
const config = await fetch('./config.json').then(r => r.json());
const auth = new NBSAuth({
apiEndpoint: config.api_gateway_url,
redirectUri: window.location.origin + '/callback.html',
appId: 'your-app-id',
oauthConfig: config.oauth_config // Required — no /auth/config API call needed
});