Appearance
User Provisioning & Session Acquisition
This page documents the complete flow from zero to an authenticated session with org access. It covers the operator-only provisioning steps that must happen before a user can log in and use the Minimum Viable Flow.
When to read this page
Read this if you are setting up users for the first time, onboarding a new tenant, or need to understand the security gates between user creation and first login.
Overview
Operator (IAM) User (API Gateway)
────────────── ──────────────────
1. Create user (uas_usercreate)
2. Issue email token (uas_emailissuetoken)
3. Confirm email token (uas_emailconfirmtoken)
4. Verify user status (uas_userstatusset)
5. Mint invitation (ofm_invitationcreate)
6. Login (POST /usm/session/create)
7. Create org (POST /ofm/org/create)
8. Verify org (ofm_orgstatusset)
9. Resolve member (POST /ofm/member/resolve)
→ Continue to Minimum Viable FlowSteps 1-5 and 8 are operator-only (direct Lambda invokes, IAM-gated). Steps 6-7 and 9 are user-facing (API Gateway, session-authenticated).
0) Set placeholders
sh
export AWS_PROFILE="g3nretailstack"
export AWS_REGION="us-east-1"
export API_BASE="https://api.g3nretailstack.com"
export EMAIL="user@example.com"
export PASSCODE='Abcd!234'1) Create user (operator-only)
Direct Lambda invoke via uas_usercreate. Creates a user with unverified status.
sh
aws lambda invoke \
--function-name uas_usercreate \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"email": "user@example.com",
"passcode": "Abcd!234",
"caption": "Jane Doe",
"reason": "onboarding",
"actor": "admin"
}
PAYLOADRequired fields:
| Field | Type | Description |
|---|---|---|
email | string | Normalized (trim + lowercase). Must match /.+@.+\..+/. Globally unique. |
passcode | string | Minimum 8 characters. Must contain uppercase, lowercase, digit, and special character. |
Optional: caption (display name, defaults to "user"), reason, actor, orgcode, cccode.
Response:
json
{
"success": true,
"data": { "user_id": "U...", "account_ref": "..." },
"revision": "REV_GUID"
}Save user_id and revision for the next steps.
What happens:
- User record created with status
unverified. - Primary email record created with status
unverified. - Passcode hashed with Argon2id and stored immediately.
- Email uniqueness enforced (409 if email already exists).
Common errors:
| HTTP | Code | Cause |
|---|---|---|
| 400 | passcode-policy-failed | Passcode does not meet complexity requirements |
| 409 | duplicate-email | Email already registered |
2) Issue email verification token (operator-only)
Direct Lambda invoke via uas_emailissuetoken. Generates a 32-byte random token (base64url-encoded) with 48-hour TTL.
sh
aws lambda invoke \
--function-name uas_emailissuetoken \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"user_id": "USER_ID",
"email": "user@example.com",
"expected_revision": "REV_FROM_STEP_1",
"reason": "email-verification",
"actor": "admin"
}
PAYLOADRequired fields:
| Field | Type | Description |
|---|---|---|
user_id | string | From step 1 |
email | string | The email to verify |
expected_revision | string | Current revision from previous response |
Response:
json
{
"success": true,
"data": { "token": "BASE64_TOKEN" },
"revision": "NEW_REV"
}Save token and the new revision.
Token is returned once
The raw token is returned only in this response. Only the SHA-256 hash is stored. If you lose it, issue a new token (last-issued wins).
3) Confirm email token (operator-only)
Direct Lambda invoke via uas_emailconfirmtoken. Verifies the token and transitions the email to verified.
sh
aws lambda invoke \
--function-name uas_emailconfirmtoken \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"user_id": "USER_ID",
"token": "BASE64_TOKEN_FROM_STEP_2",
"expected_revision": "REV_FROM_STEP_2",
"reason": "email-verification",
"actor": "admin"
}
PAYLOADRequired fields:
| Field | Type | Description |
|---|---|---|
user_id | string | From step 1 |
token | string | Raw token from step 2 |
expected_revision | string | Revision from step 2 |
Response:
json
{
"success": true,
"data": { "email": "user@example.com", "status": "verified" },
"revision": "NEW_REV"
}Save the new revision.
Common errors:
| HTTP | Code | Cause |
|---|---|---|
| 400 | token-expired | Token older than 48 hours |
| 400 | token-mismatch | Token does not match stored hash |
4) Verify user status (operator-only)
Direct Lambda invoke via uas_userstatusset. Transitions user from unverified to verified.
sh
aws lambda invoke \
--function-name uas_userstatusset \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"user_id": "USER_ID",
"status": "verified",
"expected_revision": "REV_FROM_STEP_3",
"reason": "onboarding",
"actor": "admin"
}
PAYLOADPreconditions (both must be true):
- Primary email is
verified(done in step 3). - Passcode is set (done in step 1).
Response:
json
{
"success": true,
"data": { "status": "verified" },
"revision": "NEW_REV"
}User status FSM:
unverified -> verified, doomed
verified -> suspended, doomed
suspended -> verified, doomed
doomed -> (terminal)Common errors:
| HTTP | Code | Cause |
|---|---|---|
| 400 | email-not-verified | Primary email not yet verified |
| 400 | passcode-missing | Passcode not yet set |
| 400 | user-invalid-transition | FSM transition not allowed |
User is now ready to log in
After this step, the user can create sessions via the public API. If they also need org access, continue to step 5.
5) Mint invitation (operator-only)
Direct Lambda invoke via ofm_invitationcreate. Creates an org-creation invitation code.
sh
aws lambda invoke \
--function-name ofm_invitationcreate \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"caption": "Onboarding ACME Corp",
"reason": "tenant-onboarding",
"actor": "admin"
}
PAYLOADOptional fields: caption, expires_at_utc (ISO-8601; defaults to 30 days), referral_code, schedule, reason, actor.
Response:
json
{
"success": true,
"data": {
"invitation_guid": "INV_GUID",
"code": "ABC-DEF-1234",
"status": "pending",
"expires_at_utc": "2026-03-18T..."
}
}Give the invitation code to the user.
6) Login (user-facing)
The user creates a session via the public API.
sh
curl -sS -X POST "$API_BASE/usm/session/create" \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"passcode": "Abcd!234",
"caption": "web-session",
"session_label": "browser"
}'Required fields:
| Field | Type | Description |
|---|---|---|
email | string | Registered email |
passcode | string | User's passcode |
Optional: caption, session_label, ttl_seconds (1-86400, default 3600), ttl_refresh_enabled (default true).
Response:
json
{
"success": true,
"data": {
"session_guid": "SESSION_GUID",
"user_id": "USER_ID",
"status": "active",
"expires_at_utc": "2026-02-16T12:00:00.000Z",
"ttl_seconds": 3600,
"ttl_refresh_enabled": true
}
}Save session_guid — this is the bearer token for all subsequent API calls.
Auth placement
USM uses body auth — credentials go in the JSON body. All downstream org-scoped services use header auth (x-session-guid). See Headers & Identity.
Validation performed by USM:
- Email lookup via UAS table (GSI).
- User status must be
verified. - Email status must be
verified. - Passcode verified with Argon2.
- Active session count checked against per-user cap (default 1024).
Common errors:
| HTTP | Code | Cause |
|---|---|---|
| 401 | invalid-passcode | Wrong credentials or unknown email (intentionally ambiguous) |
| 403 | user-not-verified | User status is not verified |
| 403 | email-not-verified | Email status is not verified |
| 429 | too-many-sessions | Session cap reached; close existing sessions first |
Session lifecycle:
- Default TTL: 1 hour (configurable via
ttl_seconds). - Sliding TTL: each
POST /usm/session/validatecall extends expiry ifttl_refresh_enabledis true. - Close:
POST /usm/session/closewithsession_guidin body. - Doom: sessions are doomed (terminal) on close, expiry, or user status change.
7) Create org (user-facing)
The user redeems the invitation code to create an organization.
sh
curl -sS -X POST "$API_BASE/ofm/org/create" \
-H 'Content-Type: application/json' \
-H "x-session-guid: $SESSION_GUID" \
-d '{
"orgcode": "ACME",
"invitation_code": "ABC-DEF-1234",
"user_guid": "USER_ID",
"caption": "Acme Corp"
}'Required fields:
| Field | Type | Description |
|---|---|---|
orgcode | string | Global unique, immutable. Format: ^[A-Z][A-Z0-9_-]{0,9}$ |
invitation_code | string | From step 5. Must be pending and not expired. |
user_guid | string | The creating user's ID |
Optional: caption, timezone (IANA), fiscal_calendar.
Response:
json
{
"success": true,
"data": {
"org_guid": "ORG_GUID",
"orgcode": "ACME",
"status": "unverified",
"owners": { "create_owner_user_guid": "...", "primary_owner_user_guid": "..." },
"cost_centre": { "cc_guid": "...", "cccode": "XXXX-XXXX-XXXX" }
},
"revision": "ORG_REV"
}Save org_guid and revision.
What happens atomically:
- Invitation marked
acceptedwithused_bydetails. - Org created in
unverifiedstatus. - Master cost centre auto-allocated.
- Creator registered as
create_owner+primary_owner. - Creator registered as active member.
Org is unverified
The org starts in unverified status. All org-scoped writes are blocked until an operator verifies it (step 8). Reads work, but writes return 403 org-write-blocked.
8) Verify org (operator-only)
Direct Lambda invoke via ofm_orgstatusset. Transitions org from unverified to verified.
sh
aws lambda invoke \
--function-name ofm_orgstatusset \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"org_guid": "ORG_GUID",
"status": "verified",
"expected_revision": "ORG_REV_FROM_STEP_7",
"reason": "onboarding",
"actor": "admin"
}
PAYLOADOrg status FSM:
unverified -> verified, frozen, doomed
verified <-> parked, suspended
parked -> frozen, doomed
suspended -> frozen, doomed
frozen -> doomed
doomed -> (terminal)After verification, all org-scoped services (PVM, PMC, ICS, PPM, SCM, PCM, CRM, etc.) accept writes for this org.
9) Resolve member (user-facing)
The user confirms their org membership and roles.
sh
curl -sS -X POST "$API_BASE/ofm/member/resolve" \
-H 'Content-Type: application/json' \
-H "x-session-guid: $SESSION_GUID" \
-d '{ "orgcode": "ACME" }'Response:
json
{
"success": true,
"data": {
"org_guid": "ORG_GUID",
"orgcode": "ACME",
"org_status": "verified",
"user_guid": "USER_ID",
"is_owner": true,
"member_state": "active",
"roles": ["owner"]
}
}The user is now fully provisioned with an authenticated session and org access. Continue to the Minimum Viable Flow for product creation, publishing, and beyond.
Optimistic concurrency
All state-changing UAS operations require expected_revision:
- Missing revision returns
428 expected-revision-requiredwithcurrent_revisionin the error details. - Stale revision returns
409 conflictwith bothprovided_revisionandcurrent_revision. - Each successful operation returns a new
revision— use it for the next call.
Passcode management (operator-only)
Reset a user's passcode via uas_passcodeset (initial) or uas_passcodereset (reset):
sh
aws lambda invoke \
--function-name uas_passcodereset \
--cli-binary-format raw-in-base64-out \
--payload file:///dev/stdin \
--profile "$AWS_PROFILE" \
/dev/stdout <<'PAYLOAD'
{
"user_id": "USER_ID",
"passcode": "NewPass!456",
"expected_revision": "CURRENT_REV",
"reason": "password-reset",
"actor": "admin"
}
PAYLOADPasscode policy:
- Minimum 8 characters.
- Must contain: uppercase letter, lowercase letter, digit, special character (
!@#$%^&*()-_=+?/><,."':;~). - Cannot reuse any passcode from the last 90 days.
Passcode reset revokes all sessions
Changing a passcode emits a passcodeReset event. USM listens to this event and dooms all active sessions for the user. The user must log in again.
User status check (public API)
If you need to verify credentials or check user status without creating a session, use the UAS stat endpoint:
sh
curl -sS -X POST "$API_BASE/uas/stat" \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"passcode": "Abcd!234"
}'This returns the user's status, emails, and payment methods. It does not create a session.
Adding team members
After the org is verified, the owner can invite other users:
Invite a member (owner)
sh
curl -sS -X POST "$API_BASE/ofm/member/invite/create" \
-H 'Content-Type: application/json' \
-H "x-session-guid: $SESSION_GUID" \
-d '{
"org_guid": "ORG_GUID",
"invitee_user_guid": "OTHER_USER_ID",
"role_profile_id": "inventory_clerk",
"grants": ["facility:zones_write"],
"reason": "team-expansion"
}'Accept invitation (invitee)
sh
curl -sS -X POST "$API_BASE/ofm/member/invite/accept" \
-H 'Content-Type: application/json' \
-H "x-session-guid: $INVITEE_SESSION_GUID" \
-d '{ "code": "INVITE_CODE" }'The invitee must already be a verified user with an active session. The invitee_user_guid on the invitation must match the session's user.
Quick reference: all Lambda functions
| Function | Purpose | Required fields |
|---|---|---|
uas_usercreate | Create user | email, passcode |
uas_emailissuetoken | Issue email verification token | user_id, email, expected_revision |
uas_emailconfirmtoken | Confirm email token | user_id, token, expected_revision |
uas_userstatusset | Set user status | user_id, status, expected_revision |
uas_passcodeset | Set passcode (initial) | user_id, passcode, expected_revision |
uas_passcodereset | Reset passcode | user_id, passcode, expected_revision |
ofm_invitationcreate | Mint org invitation | (all optional) |
ofm_orgstatusset | Set org status | org_guid, status, expected_revision |
All operator Lambdas require AWS IAM credentials (--profile g3nretailstack). They are not exposed via API Gateway.
Related pages
- Minimum Viable Flow — login -> org -> product -> publish
- Integration Walkthroughs — multi-service copy-paste flows
- Headers & Identity — auth placement (body vs header)
- State Machines — FSM reference for all entities
- Roles & Governance — owner/member role model
- Troubleshooting — 404 anti-enumeration, 403 org-write-blocked