Skip to content

G3N/Inventory — Mobile App Developer Guide

This document is the single reference for building the G3N/Inventory Android mobile app. It covers authentication, session lifecycle, org/facility context selection, role-based capability gating, every backend endpoint the app calls, barcode scanning patterns, error handling, and idempotency.

App profile: Android-only (phones, tablets, Zebra TC-series), online-only (no offline/background processing), camera-based barcode scanning, first-party, user session auth. No push notifications — user refreshes manually.

Stack base URL: https://api.g3nretailstack.com


Business Requirements

Functional Requirements

FR-1: Authentication & Context

IDRequirementPriority
FR-1.1User logs in with email and passcode. No SSO, no biometric — email+passcode only.Must
FR-1.2After login, user sees a list of organizations they belong to (as owner or member). User selects one.Must
FR-1.3After selecting an org, user sees a list of logical facilities they have access to within that org. User selects one.Must
FR-1.4After selecting a facility, the app determines which functions the user can access based on their facility-scoped roles. Functions the user cannot access are hidden (not grayed out — hidden).Must
FR-1.5If the user belongs to only one org, skip the org picker and auto-select. Same for single-facility.Should
FR-1.6User can switch org or facility without logging out (return to picker).Must
FR-1.7Session remains active for the duration of use without re-login as long as the user is active (1-hour sliding window, auto-extends).Must
FR-1.8On session expiry or revocation, the app navigates to the login screen with a clear message. No silent failures.Must

FR-2: Barcode Scanning

IDRequirementPriority
FR-2.1The app provides a barcode scanner (camera-based) as the primary input method for product identification.Must
FR-2.2Supported barcode formats: EAN-13, EAN-8, UPC-A, UPC-E, Code 128, Code 39, QR Code, DataMatrix, GS1-128.Must
FR-2.3On scan, the app resolves the barcode to a product variant within 500ms (including network round-trip). If resolution fails, allow manual entry of product code or identifier.Must
FR-2.4After barcode resolution, the app displays: product name/description, variant attributes (color, size, etc.), current stock level at the selected facility, and a thumbnail image (if available).Must
FR-2.5The app caches recent barcode→variant mappings locally (LRU, 500 entries, 5-minute TTL) to avoid repeated lookups for the same item during a work session.Should
FR-2.6Continuous scan mode: user can scan items in rapid succession without dismissing the scanner between scans (e.g., during receiving or counting).Should
FR-2.7Camera-based scanning must work in low-light warehouse conditions. Use torch/flashlight toggle in the scanner UI.Must
FR-2.8On Zebra TC-series devices, accept scan input from the built-in imager via DataWedge intent broadcast as a secondary input path alongside camera. No other hardware scanner integrations.Should

FR-3: Purchase Order Receiving

IDRequirementPriority
FR-3.1User can view a list of open/issued purchase orders for the selected facility, filtered by status. PO list shows human-readable vendor name (or vendor code) and issued date for quick identification.Must
FR-3.2User can select a PO and see its line items with ordered quantities, previously received quantities, and remaining quantities.Must
FR-3.3User can record a partial receipt against a PO — receiving some lines or partial quantities on a line. The system tracks cumulative received vs. ordered.Must
FR-3.4For each line being received, user can specify: quantity received, quantity rejected, and rejection reason from the RECEIVING.* taxonomy (8 codes: DAMAGED, DEFECTIVE, WRONG_ITEM, SHORT_SHIPMENT, QUALITY_ISSUE, EXPIRED, PACKAGING, OTHER). PCM validates the reason code server-side against the shared allowlist.Must
FR-3.5After recording a PCM receipt, the app also records the corresponding ICS inbound receipt (stock entry into the receiving zone/bin). These are two API calls — PCM receipt then ICS receive.Must
FR-3.6User can mark a received shipment for QC inspection or complete QC directly (accepted/rejected/partial).Should
FR-3.7User can initiate putaway after receiving: scan destination bin barcode, confirm variant and quantity, record the putaway movement from receiving zone to storage bin.Must
FR-3.8Each receiving action generates an idempotency key so that network retries do not create duplicate receipts.Must
FR-3.9The app detects over-receipt (received total exceeds ordered quantity) and displays a warning. Over-receipt is allowed (not blocked) but flagged prominently so the user can review with the vendor.Must
FR-3.10After recording a receipt, the app shows a collapsible receipt history for the PO — each receipt entry shows timestamp, line count, total received, total rejected, and receipt ID. Expanding a receipt shows per-line detail (variant, quantity received, quantity rejected, rejection reason).Must

FR-4: Return to Vendor (RTV)

IDRequirementPriority
FR-4.1User can create a new RTV when rejecting received goods. The RTV references the original PO receipt and rejected line items.Must
FR-4.2User can view open/pending RTV requests and fulfill them (mark items as shipped back to vendor).Must
FR-4.3Each RTV line specifies: variant, quantity, reason code from RECEIVING.* taxonomy (DAMAGED, DEFECTIVE, WRONG_ITEM, QUALITY_ISSUE, EXPIRED, PACKAGING, SHORT_SHIPMENT, OTHER), and free-text notes. RTV reuses the same taxonomy as receiving rejection.Must
FR-4.4RTV creation links back to the source receipt via source_refs for full traceability.Must
FR-4.5The app shows a confirmation preview before RTV submission: "Return {qty}× {variant_code} → {vendor_name} ({reason_label})".Must
FR-4.6Vendor selection uses a dropdown populated from PVM /vendor/list (cached per session). Falls back to manual text input if vendor list unavailable. Shows vendor name + code in dropdown options.Must

FR-5: Transfer Management

IDRequirementPriority
FR-5.1User can view inbound transfers (shipments destined for the current facility) with status and expected items.Must
FR-5.2User can receive an inbound transfer: scan items, confirm quantities received, record discrepancies (short shipment, damaged items, wrong items).Must
FR-5.3For discrepancies, user records exceptions with quantities and reasons. Exceptions generate comments for follow-up and may trigger adjustments.Must
FR-5.4User can create outbound transfer requests (request stock to be sent from another facility, or fulfill a request to send stock to another facility).Must
FR-5.5User can record transfer shipment: mark items as picked, packed, and shipped for an approved transfer.Must
FR-5.6User with ics_transfer_approve role can approve pending transfer requests from within the app.Should
FR-5.7Transfers display source and destination facility names (resolved from facility context cache, not raw GUIDs).Must
FR-5.8Transfer list shows human-readable facility names, dates, line counts, and status badges with color coding (submitted=yellow, approved=blue, shipped=purple, received=green).Must
FR-5.9Transfer detail view shows status timeline (requested → approved → shipped → received with timestamps), per-line breakdown, and collapsible shipment history.Must
FR-5.10Transfer create form uses a facility dropdown (populated from context cache) instead of raw GUID input. Shows confirmation preview: "{source} → {dest} · {qty}× {variant}".Must

FR-6: Stock Lookup & Stock Cards

IDRequirementPriority
FR-6.1User can look up current stock levels for any variant at the selected facility. Search by barcode scan, product code, or text search.Must
FR-6.2Stock lookup displays: on-hand, available, reserved, committed, allocated, in-transit, quarantine, damaged, and consignment quantities, plus zone/bin breakdown (if available).Must
FR-6.3User can view the stock card (ledger history) for a variant — chronological list of all movements (receives, adjustments, transfers, picks, etc.) with timestamps, actors, and quantities.Must
FR-6.4Stock card entries are paginated. User can scroll to load more history.Must
FR-6.5Cost data (unit cost, total value) is displayed only if the user has cost visibility roles (cost_view, ics_cost_admin, or finance_audit). Otherwise cost columns are completely hidden.Must
FR-6.6Stock card entries show human-readable action labels (e.g., "Received", "Adjusted", "Putaway") instead of raw type codes. Reason codes display as colored taxonomy badges.Must
FR-6.7Stock card shows a ledger summary above the table: total in, total out, net change, and entry count for the visible window.Must

FR-7: Quantity Adjustments

IDRequirementPriority
FR-7.1User can create inventory adjustments: increase or decrease stock for a variant at the current facility.Must
FR-7.2Each adjustment requires: variant (via barcode scan or lookup), quantity delta (positive or negative), reason code (from canonical list: INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.ADJUST, INVENTORY.RECALL, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO, INVENTORY.PUTAWAY), and free-text reason.Must
FR-7.3User can transition stock between statuses (e.g., available → damaged, damaged → available) with quantity and reason.Must
FR-7.4Adjustments are audited: the system records who made the adjustment, when, and why (via actor, reason, source_refs in the API).Must
FR-7.5Activity history shows actor display names (resolved from OFM, cached per session) instead of raw GUIDs. Ledger entries display human-readable action labels, reason badges, and zone/bin paths.Must
FR-7.5After scanning a product, the app loads and displays recent activity history (stock card entries) for the variant below the form. Entries show action type, quantity delta, reason code badge, and timestamp.Must
FR-7.6Large-delta warning: ifdelta
FR-7.7After recording an adjustment, the app refreshes both stock levels and activity history to show the updated state. Inline confirmation: "Adjusted {variant_code}: +/-{delta} units ({reason_label})".Must

FR-8: Flag for Review (Comments)

IDRequirementPriority
FR-8.1User can flag any product's inventory for review by adding a comment. The comment is attached to the stock entity (variant at facility).Must
FR-8.2Comments include: free-text body (up to 2,000 characters), reason tag (discrepancy, damage, shelf-life, location-error, quality, recount, other), and the variant + facility context.Must
FR-8.3Existing comments are displayed FIRST (history-first pattern) before the compose form. Shows comment count with open/unresolved count badge. Each comment shows: body, relative timestamp, author display name (resolved from OFM, cached per session), hashtag badges, and status if not current.Must
FR-8.4User can revise their own comments (edit body text).Should
FR-8.5Comments are visible to all users with read access at that facility — they serve as a communication channel between shifts/teams.Must

FR-9: Product Information

IDRequirementPriority
FR-9.1User can look up product details by scanning a barcode, searching by code, or browsing.Must
FR-9.2Product detail screen shows: style name, variant attributes (color, size, material), variant code, all registered barcodes, identifiers (UPC, GTIN, vendor SKU), status (with color-coded badge), and product images. Primary barcode is highlighted.Must
FR-9.3Identifier resolution supports multiple schemes: scan a vendor label, warehouse label, or standard barcode and the app resolves to the same variant.Should
FR-9.4Product lookup provides three tabs: Details (variant info + barcodes), Stock (current stock levels at this facility via ICS), and Activity (recent stock card ledger).Must

Non-Functional Requirements

NFR-1: Performance

IDRequirementTarget
NFR-1.1Barcode scan → product display (including stock level)< 500ms end-to-end (scan to pixels on screen)
NFR-1.2Stock lookup response time< 300ms (API round-trip)
NFR-1.3Mutation operations (receive, adjust, transfer)< 1s (API round-trip)
NFR-1.4App cold start to login screen< 2s
NFR-1.5App warm resume (session still valid) to last screen< 500ms
NFR-1.6Scrolling and list rendering60fps, no jank. Stock card and PO line lists must support 500+ items without lag.
NFR-1.7Concurrent users per facilitySystem supports 50+ concurrent app users per facility without degradation.

NFR-2: Reliability

IDRequirementTarget
NFR-2.1All mutation operations use idempotency keys. Network retry on timeout must not create duplicates.Zero duplicate transactions from retries
NFR-2.2Transient network errors (timeout, 429, 5xx) are retried automatically with exponential backoff (max 3 attempts).Transparent retry for transient failures
NFR-2.3Non-transient errors (400, 403, 404, 409) are surfaced to the user immediately with actionable messages. No silent swallowing.Every error visible to user
NFR-2.4If the backend is in maintenance mode (503), display a clear maintenance message. Do not retry.Graceful maintenance handling

NFR-3: Security

IDRequirementTarget
NFR-3.1Session token stored in Android Keystore (hardware-backed where available). Never in SharedPreferences or plain storage.Hardware-backed credential storage
NFR-3.2No credentials (passcode, session_guid, API keys) written to application logs, crash reports, or analytics.Zero credential leakage
NFR-3.3All network communication over HTTPS (TLS 1.2+). Certificate pinning for api.g3nretailstack.com recommended.Encrypted transport
NFR-3.4Role-based UI gating is defense-in-depth only. The server enforces all access control (403 on violation). Never rely solely on client-side hiding.Server-authoritative access control
NFR-3.5Cost data (unit cost, margin, total value) is never displayed, cached, or logged unless the user has explicit cost visibility roles.Cost data isolation
NFR-3.6On session expiry or 401, immediately clear all cached data and stored session. Navigate to login.Clean session teardown

NFR-4: Usability

IDRequirementTarget
NFR-4.1The app is designed for one-handed operation on standard Android phones (5.5"–6.7" screens). Primary actions reachable by thumb.One-handed warehouse use
NFR-4.2Large touch targets for all interactive elements (minimum 48dp). Warehouse workers wear gloves.Glove-friendly interaction
NFR-4.3High-contrast UI. Readable in bright warehouse lighting and dim stockroom conditions. Support system dark/light mode.All-environment readability
NFR-4.4Barcode scanner activates with a single tap or hardware button. No multi-step navigation to reach the scanner from any screen.Instant scanner access
NFR-4.5Confirmation dialogs for destructive or high-impact actions (adjustments, RTV creation, transfer approval). Quick actions (scan, lookup) require no confirmation.Appropriate confirmation friction
NFR-4.6Clear visual feedback for all operations: success (green check/toast), error (red banner with message), loading (spinner with operation name).Unambiguous status feedback
NFR-4.7All quantities displayed with UOM (unit of measure). Never display a bare number.Unambiguous quantity display

NFR-5: Platform & Compatibility

IDRequirementTarget
NFR-5.1Android minimum SDK: API 26 (Android 8.0 Oreo). Target SDK: latest stable.Broad device support
NFR-5.2Supported device types: low-end to high-end Android handhelds (phones) and Android tablets. Also Zebra TC-series rugged devices (TC21, TC26, TC52, TC57, TC72, TC77 and successors). No Honeywell, no Datalogic.Device range: budget phone → enterprise rugged
NFR-5.3Camera is the primary barcode scanner. The app uses the device camera (via ML Kit, ZXing, or similar) for all barcode scanning. No dependency on integrated hardware scanners. On Zebra TC-series devices, the app should also accept scan input from the built-in imager via Zebra DataWedge intent broadcast as a secondary input path for faster scanning in high-volume workflows.Camera-first scanning, Zebra DataWedge optional
NFR-5.4The app must function well on small screens (5" budget phone) and large screens (10" tablet). Layouts adapt to screen size. Tablet layout may show master-detail (e.g., PO list + line items side by side).Responsive layout: phone + tablet
NFR-5.5Language: English (en_US) only for v1. All server error messages arrive in en_US.English only
NFR-5.6Network: requires active network connection (Wi-Fi or cellular). No offline mode. Clear error when network unavailable.Online-only with clear offline indicator

NFR-6: Observability & Support

IDRequirementTarget
NFR-6.1Every API response's stats.request_id and stats.build.build_id is logged locally (ring buffer, last 20 requests).Request traceability
NFR-6.2The X-API-Version response header is logged per-session. If it changes mid-session, log a warning (backend was redeployed).Version drift detection
NFR-6.3A "Support" screen shows: current session info (user, org, facility, session age), last 10 request IDs, API version, app version, device model. User can copy this to clipboard for support tickets.Self-service diagnostics
NFR-6.4Crash reporting (e.g., Firebase Crashlytics) enabled with session metadata (org, facility, app version) but NOT session_guid or passcode.Safe crash reporting

NFR-7: Data Handling

IDRequirementTarget
NFR-7.1No local persistent storage of business data (stock levels, POs, products). All data is fetched from the API on demand.No stale local data
NFR-7.2Barcode→variant cache is in-memory only (cleared on app kill or session change).Ephemeral cache only
NFR-7.3Pagination cursors (next_token) are opaque and treated as ephemeral. Never persisted across sessions.Cursor hygiene
NFR-7.4The app does not poll or auto-refresh. User pulls to refresh or taps a refresh button.Manual refresh only

1. Architecture Overview

The app talks directly to the g3nretailstack API Gateway — no integration plane, no BFF. Every service is reachable at https://api.g3nretailstack.com/{service}/....

┌─────────────────────┐
│  G3N/Inventory App   │
│  (Android / Kotlin)  │
└──────────┬──────────┘
           │  HTTPS (JSON)

┌──────────────────────────────────────────┐
│  api.g3nretailstack.com                   │
│  ┌─────┬─────┬─────┬─────┬─────┬──────┐ │
│  │ USM │ UAS │ OFM │ ICS │ PCM │ PVM  │ │
│  └─────┴─────┴─────┴─────┴─────┴──────┘ │
└──────────────────────────────────────────┘

Services the app uses:

ServicePurposeBase path
USMSession creation and management/usm
UASUser snapshot (org memberships)/uas
OFMOrg/facility/role resolution/ofm
ICSInventory operations (receive, adjust, transfer, stock)/ics
PCMProcurement (PO receive, RTV)/pcm
PVMProduct/barcode lookup/pvm

2. Authentication & Session Lifecycle

2.1 Login

POST /usm/session/create
Content-Type: application/json

{
  "email": "user@merchant.com",
  "passcode": "their-passcode",
  "ttl_seconds": 3600,
  "caption": "G3N/Inventory Android"
}

Response (200):

json
{
  "success": true,
  "data": {
    "session_guid": "sess-abc123...",
    "user_id": "u-def456...",
    "status": "active",
    "expires_at_utc": "2026-03-04T14:30:00Z",
    "ttl_seconds": 3600
  }
}

Store session_guid securely (Android Keystore). This is the bearer token for all subsequent calls.

Session properties:

  • TTL: 1 hour, sliding — auto-extends on every API call. An active warehouse worker never gets logged out mid-shift.
  • Expiry: If idle for 1 hour, session expires. App must detect 401 and prompt re-login.
  • Revocation: Sessions can be revoked server-side (logout-everywhere, user suspended). Detect via 401.

2.2 Request Headers (every call after login)

HeaderRequiredValue
x-session-guidYesThe session_guid from login
x-orgcodeYes (after org selected)Uppercase org code, e.g. ACMECORP
x-logical-guidYes (after facility selected)Facility GUID for scoped operations
x-cccodeOptionalCost centre code (format: XXXX-XXXX-XXXX)
Content-TypeYes (POST)application/json

2.3 Response Envelope (every response)

Every response follows this shape:

json
{
  "success": true,
  "data": { ... },
  "revision": "rev-abc123",
  "stats": {
    "service": "ics",
    "call": "ics_stock_get",
    "request_id": "req-xyz",
    "timestamp_utc": "2026-03-04T13:30:00Z",
    "build": { "build_id": "MONDAY-1770260725" }
  }
}

Always log stats.request_id and stats.build.build_id — these are required for support tickets.

Response headers: Every response includes X-API-Version: YYYY-MM-DD (current: 2026-03-04). Log this for version tracking.

2.4 Explicit Logout

POST /usm/session/close
Content-Type: application/json

{ "session_guid": "sess-abc123..." }

Response: { data: { session_guid, status: "doomed", doom_reason: "closed" } }. After closing, clear all local state.

2.5 Session Expiry Detection

On any 401 response with tag unauthorized or invalid-session:

  1. Clear stored session
  2. Navigate to login screen
  3. Do not retry the failed request automatically

3. Org & Facility Selection Flow

After login, the app must determine which org and facility the user is acting on.

3.1 Get User Snapshot

POST /uas/stat
Content-Type: application/json

{
  "email": "user@merchant.com",
  "passcode": "their-passcode"
}

Returns the user's profile (emails, passcode state). Note: /uas/stat uses email+passcode auth (public endpoint), not session headers.

Org discovery: The user snapshot does not include org memberships directly. The app should call /ofm/member/resolve for each known orgcode to verify membership, or maintain a list of orgcodes from a prior session. For first-time setup, the user provides their orgcode(s) and the app validates via /ofm/member/resolve.

3.2 Resolve Membership & Roles for Selected Org

POST /ofm/member/resolve
x-session-guid: sess-abc123...
Content-Type: application/json

{
  "orgcode": "ACMECORP"
}

Returns:

json
{
  "success": true,
  "data": {
    "org_guid": "org-12345",
    "orgcode": "ACMECORP",
    "org_status": "verified",
    "is_owner": false,
    "member_state": "active",
    "roles": ["ics_operator", "ics_view", "pcm_buyer", "pvm_view"],
    "logical_access": false
  }
}

If org_status is not verified, the org is read-only (writes blocked with 403 org-write-blocked).

3.3 List Facilities

POST /ofm/facility/logical/list
x-session-guid: sess-abc123...
x-orgcode: ACMECORP
Content-Type: application/json

{
  "org_guid": "org-12345",
  "status": "active"
}

Note: This endpoint requires org_guid (from the /ofm/member/resolve response), not orgcode. It also requires owner-level access. For non-owner members, the app should use /ofm/member/resolve with each known logical_guid to check facility access, or the backend should provide a member-scoped facility list.

Returns { facilities: [...] } where each facility includes logical_guid, code, caption, physical_guid, legal_guid, status. Build a facility picker from this.

3.4 Resolve Facility-Scoped Roles

After user picks a facility, resolve roles again with logical_guid:

POST /ofm/member/resolve
x-session-guid: sess-abc123...
Content-Type: application/json

{
  "orgcode": "ACMECORP",
  "logical_guid": "lq-facility-001"
}

Returns facility-scoped roles in logical_roles. If logical_access is false, the user has no assignment to this facility — show an error.

Key fields to check:

  • logical_access: true — user has facility assignment
  • logical_roles — facility-scoped roles (may differ from org-level roles)
  • is_owner: true — owner bypasses all role checks

4. Role-Based Capability Mapping

After facility selection, derive the app's feature flags from the user's roles. The server enforces roles on every call (403 on violation), but the app should hide UI for inaccessible features.

4.1 Role → Feature Map

kotlin
data class Capabilities(
    val receiving: Boolean,          // PO receiving + ICS receive
    val putaway: Boolean,            // Move stock to storage
    val pickPackShip: Boolean,       // Fulfillment operations
    val adjustments: Boolean,        // Qty adjustments, stock transitions
    val counts: Boolean,             // Inventory cycle counts
    val transferCreate: Boolean,     // Create transfer requests
    val transferApprove: Boolean,    // Approve transfer requests
    val transferReceive: Boolean,    // Receive inbound transfers
    val rtvCreate: Boolean,          // Return to vendor
    val stockLookup: Boolean,        // View stock levels + cards
    val productLookup: Boolean,      // Product info + barcode scan
    val comments: Boolean,           // Flag for review with comments
    val poLookup: Boolean,           // View purchase orders
    val costView: Boolean            // See cost data
)

fun deriveCapabilities(roles: List<String>, isOwner: Boolean): Capabilities {
    if (isOwner) return Capabilities(/* all true */)
    return Capabilities(
        receiving       = "ics_operator" in roles || "pcm_buyer" in roles,
        putaway         = "ics_operator" in roles,
        pickPackShip    = "ics_operator" in roles,
        adjustments     = "ics_adjust" in roles || "ics_operator" in roles,
        counts          = "ics_count" in roles,
        transferCreate  = "ics_operator" in roles,
        transferSubmit  = "ics_transfer_approve" in roles,
        transferApprove = "ics_transfer_approve" in roles,
        transferReceive = "ics_operator" in roles,
        rtvCreate       = "pcm_buyer" in roles,
        stockLookup     = roles.any { it.startsWith("ics_") },
        productLookup   = "pvm_view" in roles || roles.any { it.startsWith("ics_") },
        comments        = "ics_operator" in roles || "ics_planner" in roles || "ics_adjust" in roles,
        poLookup        = "pcm_view" in roles || "pcm_buyer" in roles || "pcm_po_approve" in roles || "finance_audit" in roles,
        costView        = "cost_view" in roles || "ics_cost_admin" in roles || "finance_audit" in roles
    )
}

4.2 Standard Role Profiles

OFM assigns role profiles to members. These expand server-side:

ProfileRoles Granted
warehouse_staffics_view, ics_operator, ics_count
warehouse_managerics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, pcm_view, pcm_buyer
store_managercrm_view, crm_manage, ics_view, ics_operator, scm_view, scm_order, scm_fulfillment, scm_returns, ppm_view, pcm_view
buyerpvm_view, pcm_view, pcm_buyer, ppm_view, ics_view

A warehouse_staff user sees: stock lookup, receiving, putaway, counts. No adjustments, no transfers approve, no RTV. A warehouse_manager sees everything.


5. Barcode Scanning

Barcode scanning is the primary input method. The scan→resolve→action chain must be fast.

5.1 Resolve Barcode

GET /pvm/barcode/resolve?orgcode=ACMECORP&value=5901234123457
x-session-guid: sess-abc123...
x-orgcode: ACMECORP

Response (200):

json
{
  "success": true,
  "data": {
    "barcode": {
      "barcode_id": "bc-001",
      "variant_id": "v-red-shirt-s",
      "value": "5901234123457",
      "scheme": "EAN13",
      "packaging_level": "unit",
      "is_primary": true,
      "status": "active"
    },
    "owner": {
      "variant_id": "v-red-shirt-s",
      "variant_code": "SHIRT-RED-S"
    }
  }
}

Latency target: ~100ms (Tier B SLO). Fire this immediately on scan.

GTIN-only: /pvm/barcode/resolve only accepts GTIN-standard barcodes (8, 12, 13, or 14 digit codes with valid check digit — EAN-8, UPC-A, EAN-13, GTIN-14). For non-GTIN codes (Code 128, Code 39, QR, DataMatrix, vendor SKUs), use /pvm/identifier/resolve instead.

5.2 Resolve Identifier (UPC, vendor SKU, etc.)

GET /pvm/identifier/resolve?orgcode=ACMECORP&type=sku&value=VENDOR-SKU-123
x-session-guid: sess-abc123...
x-orgcode: ACMECORP

Use when the scanned code isn't a standard barcode (e.g., vendor-specific label).

5.3 Get Product Details

After resolving variant_id:

GET /pvm/variant/get?orgcode=ACMECORP&variant_id=v-red-shirt-s&style_id=s-red-shirt
x-session-guid: sess-abc123...
x-orgcode: ACMECORP

Returns full variant details: name, color, size, images, status, cost (if cost_view role), etc.

5.4 Configurable Kits

Variants can have a kit_type field: standard (fixed BOM) or configurable (customer picks components from slots). The app should detect kit_type on resolved variants and adjust workflows accordingly.

Key endpoints (all require pvm_view or equivalent):

MethodPathPurpose
POST/pvm/kit/slot/addAdd a slot to a configurable kit variant
GET/pvm/kit/slot/listList slots for a kit variant
POST/pvm/kit/slot/choice/addAdd a variant choice to a slot
GET/pvm/kit/slot/choice/listList available choices for a slot
POST/pvm/kit/configure/validateValidate a customer's kit configuration against rules
POST/pvm/kit/rule/addAdd a constraint rule (inclusion/exclusion)
GET/pvm/kit/rule/listList rules for a kit variant

Impact on inventory workflows:

  • Receiving: Kit variants with kit_type=standard are received as a single unit; SCM explodes them into component stock positions automatically.
  • Transfers: Same explosion behavior applies — transfer the kit SKU, and ICS tracks component-level stock.
  • Configurable kits: Not typically stocked as assembled units. The app receives/transfers individual component variants. Validation (/pvm/kit/configure/validate) is used at order time, not at receiving time.

5.5 Performance Pattern

For scan-and-display:

  1. Scan → call /pvm/barcode/resolve (~100ms)
  2. Parallel → call /ics/stock/get with variant_id + logical_guid (~250ms)
  3. Display: product name + current stock level within ~350ms total

Cache recommendation: LRU cache for barcode→variant mappings (5-min TTL, 500 entries). Product info changes rarely; stock levels should not be cached.


6. Inventory Operations (ICS)

All ICS mutation endpoints require x-logical-guid header. All support optional idempotency_key for safe retry.

request_context on mutations. Most ICS mutations derive request_context automatically from the x-session-guid and x-orgcode headers — you do not need to include it in the request body. The exceptions are endpoints that validate a nested request_context in the body:

  • POST /ics/transfer/request/create — requires transfer.request_context
  • POST /ics/reservation/create — requires reservation.request_context
  • POST /ics/allocation/create — requires allocation.request_context
  • POST /ics/commit/create — requires commit.request_context

For those endpoints, include:

json
{
  "transfer": {
    "request_context": {
      "session_guid": "<from login>",
      "orgcode": "ACMECORP",
      "actor": "<user_guid>",
      "context_source": "session",
      "session_fingerprint": "<SHA-256 hex of 'session_guid:<value>'>"
    },
    ...other transfer fields...
  }
}

The session_fingerprint is SHA256("session_guid:" + sessionGuid) as a lowercase hex string. Compute it once when the session is created.

policy_versions required on mutations. Adjustment, transition, receiving, and transfer endpoints require a policy_versions object (e.g., { "adjustment_policy_version": "default" }). Use "default" until custom policies are configured.

Reason codes. All reason_code fields must use the canonical allowlist format: INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.ADJUST, INVENTORY.RECALL, INVENTORY.PUTAWAY, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO. Lowercase or unscoped codes (e.g., "shrinkage") are rejected.

6.1 Stock Lookup

POST /ics/stock/get
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{ "variant_id": "v-red-shirt-s" }

Roles required: ics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, ics_cost_admin, finance_audit

POST /ics/stock/list
{ "limit": 20, "next_token": null }

6.2 Stock Card (Ledger History)

POST /ics/stock/card/list
{ "variant_id": "v-red-shirt-s", "limit": 20 }

Returns the chronological ledger of all stock movements for a variant at the current facility.

6.3 Receiving (ICS side)

POST /ics/receive/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "receipt": {
    "lines": [
      {
        "variant_id": "v-red-shirt-s",
        "qty": { "qty": 50, "uom": "EACH" }
      }
    ],
    "receiving_zone_code": "RECEIVING",
    "receiving_bin_code": "RECEIVING-01",
    "qc_required": false,
    "policy_versions": { "receiving_policy_version": "default" }
  },
  "reason": "PO-2026-001 partial receipt",
  "source_refs": [{ "kind": "po", "id": "po-guid-001" }],
  "idempotency_key": "recv-po001-line3-attempt1"
}

Notes: Multi-line receiving is supported (up to 20 lines per call). qc_required: true puts received items into QC hold status.

Roles required: ics_operator

6.4 QC Complete

POST /ics/qc/complete
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "qc": {
    "receipt_id": "rcv-guid-001",
    "lines": [
      {
        "line_id": "line-001",
        "passed_qty": { "qty": 47, "uom": "EACH" },
        "failed_qty": { "qty": 3, "uom": "EACH" }
      }
    ]
  },
  "reason": "Visual inspection — 3 units with torn packaging",
  "source_refs": [{ "kind": "receive", "id": "rcv-guid-001" }]
}

Notes: QC is per-line with separate passed_qty/failed_qty (not a simple pass/fail disposition). Each line_id must match a line from the original receipt.

Roles required: ics_operator

6.5 Putaway

POST /ics/putaway/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "putaway": {
    "variant_id": "v-red-shirt-s",
    "qty": { "qty": 50, "uom": "EACH" },
    "to_zone_code": "STOCKROOM",
    "to_bin_code": "STOCKROOM-02"
  },
  "reason": "Standard putaway from receiving",
  "source_refs": [{ "kind": "receive", "id": "rcv-guid-001" }],
  "idempotency_key": "putaway-rcv001-attempt1"
}

Notes: from_zone_code/from_bin_code are not required for putaway (the system knows where the stock currently is). Only to_zone_code/to_bin_code are required.

Roles required: ics_operator

6.6 Qty Adjustment

POST /ics/adjustment/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "adjustment": {
    "variant_id": "v-red-shirt-s",
    "delta_qty": { "qty": -3, "uom": "EACH" },
    "reason_code": "INVENTORY.DAMAGE",
    "policy_versions": { "adjustment_policy_version": "default" }
  },
  "reason": "Found 3 units water-damaged in bin A-03-02",
  "source_refs": [{ "kind": "count", "id": "count-guid-001" }],
  "idempotency_key": "adj-damaged-20260304-1"
}

Valid reason codes: INVENTORY.ADJUST, INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.RECALL, INVENTORY.PUTAWAY, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO.

Roles required: ics_adjust or ics_operator

6.7 Stock Transition (bucket change)

POST /ics/stock/transition
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "transition": {
    "variant_id": "v-red-shirt-s",
    "qty": { "qty": 3, "uom": "EACH" },
    "from_bucket": "available",
    "to_bucket": "damaged",
    "reason_code": "INVENTORY.DAMAGE",
    "policy_versions": { "transition_policy_version": "default" }
  },
  "reason": "Water damage identified during count",
  "source_refs": [{ "kind": "adjustment", "id": "adj-guid-001" }]
}

Valid buckets: available, quarantine, damaged. from_bucket and to_bucket must differ.

Roles required: ics_adjust or ics_operator

6.8 Transfers

Transfer lifecycle: created → submitted → approved → shipped → received.

Create transfer request (between facilities):

POST /ics/transfer/request/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001

{
  "transfer": {
    "request_context": {
      "session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
      "context_source": "session", "session_fingerprint": "...",
      "logical_guid": "lq-warehouse-001"
    },
    "source_logical_guid": "lq-warehouse-001",
    "dest_logical_guid": "lq-store-002",
    "lines": [
      { "variant_id": "v-red-shirt-s", "qty": { "qty": 20, "uom": "EACH" } }
    ],
    "policy_refs": { "transfer_policy_version": "default" }
  },
  "reason": "Store replenishment",
  "source_refs": [{ "kind": "webapp", "id": "dev-seed" }]
}

Roles: ics_operator. This is one of the few endpoints that requires request_context nested inside the body (at transfer.request_context). Response: { data: { transfer: { transfer_id, status: "requested", ... } } }.

Submit for approval:

POST /ics/transfer/request/submit
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001

{ "transfer_id": "tr-guid-001", "expected_revision": 1, "reason": "Ready for review", "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }] }

Roles: ics_transfer_approve. Moves from requestedsubmitted.

Approve transfer:

POST /ics/transfer/request/approve
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001

{ "transfer_id": "tr-guid-001", "expected_revision": 2, "reason": "Approved", "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }] }

Roles: ics_transfer_approve. Moves from submittedapproved. Note: expected_revision is required (numeric, starts at 1, increments on each state change).

Record shipment (send from source facility):

POST /ics/transfer/shipment/record
x-logical-guid: lq-warehouse-001

{
  "shipment": {
    "transfer_id": "tr-guid-001",
    "lines": [
      { "variant_id": "v-red-shirt-s", "qty": { "qty": 20, "uom": "EACH" } }
    ]
  },
  "reason": "Shipped via internal courier",
  "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }]
}

Roles: ics_operator. Response: { data: { shipment: { shipment_id, ... } } }.

Receive shipment (at destination facility):

POST /ics/transfer/shipment/receive
x-logical-guid: lq-store-002

{
  "shipment_id": "ship-guid-001",
  "expected_revision": 1,
  "reason": "Transfer received, all units good",
  "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }]
}

Roles: ics_operator. If received_lines is omitted, all shipped lines are received in full. For partial receipt or discrepancies, include received_lines with per-line quantities. Short shipments generate FULFILLMENT.SHORT exceptions automatically.

List transfer requests:

POST /ics/transfer/request/list
{ "status": "submitted", "limit": 20 }

List shipments:

POST /ics/transfer/shipment/list
{ "transfer_id": "tr-guid-001", "limit": 20 }

6.9 Comments (Flag for Review)

POST /ics/comment
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "target_type": "stock",
  "target_id": "v-red-shirt-s",
  "body": "Stock level doesn't match shelf count. Flagging for full recount.",
  "hashtags": ["discrepancy"]
}

Response: { data: { comment: { comment_id, target_type, target_id, body, hashtags, status, created_at, ... } } }

Roles required: ics_operator, ics_planner, or ics_adjust

Additional comment operations:

EndpointRequest FieldsNotes
POST /ics/comment/getcomment_id, target_type, target_id
POST /ics/comment/listtarget_type, target_id, optional status, hashtag, limit, next_tokenPaginated
POST /ics/comment/revisecomment_id, target_type, target_id, body, optional expected_revisionAuthor-only
POST /ics/comment/statuscomment_id, target_type, target_id, status (current/archived/doomed)Author-only

Notes: Comments also support caption, attachments (MRS references), parent_comment_id (threaded replies), mentions_users, mentions_teams. At least one of caption, body, or attachments is required.


7. Procurement Operations (PCM)

7.1 PO Lookup

POST /pcm/po/get
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{ "po_id": "po-guid-001" }

Roles required: pcm_view, pcm_buyer, pcm_po_approve, finance_audit

POST /pcm/po/list
{ "status": "issued", "limit": 20 }

7.2 PO Receipt (partial or full)

POST /pcm/receipt/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "receipt": {
    "po_id": "po-guid-001",
    "request_context": {
      "session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
      "context_source": "session", "session_fingerprint": "..."
    },
    "lines": [
      {
        "po_line_id": "pol-001",
        "variant_id": "v-red-shirt-s",
        "qty": { "qty": 50, "uom": "EACH" },
        "rejected_qty": 3,
        "rejection_reason": "RECEIVING.DAMAGED"
      }
    ]
  },
  "reason": "Partial receipt - 1 of 3 pallets",
  "source_refs": [{ "kind": "po", "id": "po-guid-001" }],
  "idempotency_key": "receipt-po001-pallet1"
}

Rejection reason codes (RECEIVING.* taxonomy — server-validated by PCM):

CodeUse when
RECEIVING.DAMAGEDPhysical damage during shipping
RECEIVING.DEFECTIVEManufacturing defect
RECEIVING.WRONG_ITEMItem doesn't match PO line
RECEIVING.SHORT_SHIPMENTFewer units than BOL claims
RECEIVING.QUALITY_ISSUEFails quality standards
RECEIVING.EXPIREDPast expiry / insufficient shelf life
RECEIVING.PACKAGINGPackaging compromised
RECEIVING.OTHERCatch-all (add free-text in reason field)

Notes: The receipt body must be wrapped in a receipt object. request_context goes inside receipt, not at the top level. Response: { data: { receipt: { receipt_id, status, ... }, po_status: "received" | "partially_received" } }.

Roles required: pcm_buyer

Partial receiving is native — include the lines and quantities received in this shipment. Repeat for subsequent pallets/shipments.

PO status auto-update: When po_id is provided, the server automatically transitions the PO status based on cumulative received quantities across all receipts:

  • All PO lines fulfilled → PO status becomes received
  • Some lines still pending → PO status becomes partially_received
  • The response includes po_status so the app can update the UI immediately without re-fetching the PO.
  • Receipt default status is received (not recorded).

Note on cumulative tracking: To show received vs. ordered progress, the app can fetch receipts for the PO (via /pcm/receipt/list { po_id }) and compute the cumulative totals client-side, or simply check the PO's own status field which is auto-updated on each receipt.

7.3 Return to Vendor (RTV)

POST /pcm/rtv/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001

{
  "rtv": {
    "vendor_ref": { "vendor_code": "VENDCORP" },
    "request_context": {
      "session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
      "context_source": "session", "session_fingerprint": "..."
    },
    "lines": [
      {
        "variant_id": "v-red-shirt-s",
        "qty": 5,
        "reason_code": "damaged",
        "reason": "Water-damaged units from PO-2026-001"
      }
    ]
  },
  "reason": "Rejecting damaged goods from PO receipt",
  "source_refs": [{ "kind": "receipt", "id": "rcpt-guid-001" }],
  "idempotency_key": "rtv-po001-damaged-batch1"
}

Notes: The body must be wrapped in an rtv object. vendor_ref is an object (not a bare vendor_code string). request_context goes inside rtv. reason at the top level is required. RTV-level reason codes are not validated against the canonical allowlist (stored as-is), but we recommend using the CATEGORY.CODE format for consistency.

Roles required: pcm_buyer


8. Error Handling

8.1 HTTP Status Codes

StatusTagMeaningApp Action
200SuccessProcess response
400validation-errorInvalid inputShow field-level errors
401unauthorized, invalid-sessionSession expired/revoked→ Login screen
403forbidden-roleUser lacks required roleShow "access denied"
403org-write-blockedOrg not verifiedShow "org not active"
403(no JSON body)WAF blocked requestSee section 8.4
404not-foundRecord missing or anti-enumerationShow "not found"
409conflictRevision mismatchRe-fetch and retry
428expected-revision-requiredMissing expected_revisionRe-fetch to get current revision
429throttledRate limitedBackoff and retry after 1-5s
500internal-errorServer errorShow generic error, log request_id
502dependency-unavailableUpstream service downRetry with backoff
503maintenance-activeScheduled maintenanceShow maintenance message

8.2 Error Response Shape

json
{
  "success": false,
  "error": {
    "error_code": "ics.validation_failed",
    "http_status": 400,
    "retryable": false,
    "major": { "tag": "validation-error", "message": { "en_US": "Invalid input" } },
    "details": { "field": "qty", "reason": "must be positive" }
  },
  "stats": { "request_id": "req-xyz", ... }
}

Use error.retryable to decide whether to retry. Use error.major.tag for programmatic handling. Use error.major.message.en_US for user-facing display.

8.3 Retry Strategy

  • Retryable errors (429, 500, 502, 504): exponential backoff with jitter (100ms, 200ms, 400ms, max 3 attempts).
  • Non-retryable errors (400, 401, 403, 404, 409, 428): do not retry. Handle in the UI.
  • Network errors (timeout, connection refused): retry with backoff, max 3 attempts.
  • Mutations: always include idempotency_key so retries are safe.

8.4 WAF (Web Application Firewall)

All API endpoints and CloudFront distributions are protected by AWS WAF. WAF inspects every request at the network layer before it reaches the application.

How to distinguish WAF 403 from application 403:

  • Application 403: Returns a JSON body with error.major.tag (e.g., forbidden-role, org-write-blocked).
  • WAF 403: Returns a short HTML body (<html>... 403 Forbidden ...</html>) with no JSON. No error object, no request_id.

When WAF blocks a request:

  • The request body contained a pattern matching a known attack signature (SQL injection, XSS, JNDI injection, path traversal).
  • The caller's IP address is on a known-bad-actor list.
  • The caller exceeded 2,000 requests per 5-minute window from the same IP.

App handling:

  • If you receive a 403 with no JSON body, do not retry. Check the request payload for unusual characters or patterns.
  • If the block is unexpected, it may be a false positive from product descriptions or free-text fields containing HTML-like content. Contact support with the request timestamp and source IP.

WAF rules active:

  • AWS Managed Rules: Common Rule Set (OWASP top 10), Known Bad Inputs (Log4j, etc.), Amazon IP Reputation List.
  • Rate limit: 2,000 requests per 5-minute window per source IP.

9. Idempotency

All mutation endpoints accept an optional idempotency_key field. This prevents duplicate operations when the app retries after a network timeout.

9.1 How It Works

  1. App generates a unique key per user action (e.g., "recv-po001-line3-{uuid}").
  2. Sends it in the request body as idempotency_key.
  3. If the server already processed that key (within 24h), it returns the cached response.
  4. If the same key is sent with a different payload, server returns 409 idempotency-conflict.

9.2 Key Generation Pattern

kotlin
fun idempotencyKey(action: String, contextId: String): String {
    return "$action-$contextId-${UUID.randomUUID()}"
}

// Usage:
val key = idempotencyKey("receive", "po-001-line-3")

Generate the key once per user action (not per retry). Store it locally until the request succeeds or the user abandons the action.


10. App-Relevant Endpoint Reference

ICS — Inventory Control

CategoryMethodPathRoles
StockPOST/ics/stock/getREAD
POST/ics/stock/listREAD
POST/ics/stock/card/listREAD
POST/ics/stock/policy/getREAD
POST/ics/stock/transitionADJUST
ReceivingPOST/ics/receive/recordOPERATOR
POST/ics/receive/getREAD
POST/ics/receive/listREAD
POST/ics/qc/completeOPERATOR
PutawayPOST/ics/putaway/recordOPERATOR
MovementPOST/ics/pick/recordOPERATOR
POST/ics/pack/recordOPERATOR
POST/ics/ship/recordOPERATOR
AdjustmentPOST/ics/adjustment/createADJUST
TransfersPOST/ics/transfer/request/createOPERATOR
POST/ics/transfer/request/submitTRANSFER_APPROVE
POST/ics/transfer/request/approveTRANSFER_APPROVE
POST/ics/transfer/request/getREAD
POST/ics/transfer/request/listREAD
POST/ics/transfer/shipment/recordOPERATOR
POST/ics/transfer/shipment/receiveOPERATOR
POST/ics/transfer/shipment/getREAD
POST/ics/transfer/shipment/listREAD
POST/ics/transfer/suggestPLANNER
POST/ics/transfer/reportREAD
Bins/ZonesPOST/ics/bin/getREAD
POST/ics/bin/listREAD
POST/ics/zone/getREAD
POST/ics/zone/listREAD
CommentsPOST/ics/commentCOMMENT_WRITE
POST/ics/comment/getREAD
POST/ics/comment/listREAD
POST/ics/comment/reviseCOMMENT_WRITE
POST/ics/comment/statusCOMMENT_WRITE

Role groups: READ = ics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, ics_cost_admin, finance_audit. OPERATOR = ics_operator. ADJUST = ics_adjust, ics_operator. PLANNER = ics_planner, ics_operator. TRANSFER_APPROVE = ics_transfer_approve. COMMENT_WRITE = ics_operator, ics_planner, ics_adjust.

PCM — Procurement

CategoryMethodPathRoles
POPOST/pcm/po/getREAD
POST/pcm/po/listREAD
ReceiptPOST/pcm/receipt/recordBUYER
POST/pcm/receipt/getREAD
POST/pcm/receipt/listREAD
RTVPOST/pcm/rtv/createBUYER
POST/pcm/rtv/getREAD
POST/pcm/rtv/listREAD
CreditPOST/pcm/credit/applyPO_APPROVE

Role groups: READ = pcm_view, pcm_buyer, pcm_po_approve, finance_audit. BUYER = pcm_buyer. PO_APPROVE = pcm_po_approve.

PVM — Product Lookup

CategoryMethodPathRoles
BarcodeGET/pvm/barcode/resolveREAD
GET/pvm/barcode/listREAD
IdentifierGET/pvm/identifier/resolveREAD
GET/pvm/identifier/listREAD
ProductGET/pvm/variant/getREAD
GET/pvm/variant/listREAD
GET/pvm/styleREAD
GET/pvm/style/getREAD
GET/pvm/resolve/codeREAD

Role groups: READ = pvm_view (mapped as pvv internally). Owner access bypasses role checks.


11. Workflow Examples

11.1 Receive a PO Shipment

1. Browse open POs
   → POST /pcm/po/list { status: "issued" }
   → PO list shows vendor name + issued date for quick identification
   → User selects the matching PO

2. Review PO lines + receipt history
   → App shows line items with ordered/received/remaining quantities
   → Collapsible receipt history: each prior receipt shows timestamp,
     line count, totals, and per-line detail (expand to see)
   → Over-receipt warning: if received_total > ordered_qty on any line,
     the app flags it (warn, don't block)

3. Scan item barcode
   → GET /pvm/barcode/resolve → variant_id
   → Match against PO line

4. Record PCM receipt (with rejections)
   → POST /pcm/receipt/record {
       receipt: { po_id, request_context: {...},
         lines: [{variant_id, qty: {qty, uom:"EA"},
                  rejected_qty: N, rejection_reason: "RECEIVING.DAMAGED"}]
       }, reason: "...", source_refs: [...]
     }
   → rejection_reason must be a RECEIVING.* taxonomy code (server-validated)
   → Partial receiving: repeat for each pallet

5. ICS receiving (stock inbound)
   → POST /ics/receive/record { receipt: { lines: [{variant_id, qty: {qty, uom:"EA"}}], receiving_zone_code: "RECEIVING", receiving_bin_code: "RECEIVING-01", policy_versions: {...} }, reason: "...", source_refs: [...] }

6. QC inspection (if required)
   → POST /ics/qc/complete { qc: { receipt_id, lines: [{line_id, passed_qty: {qty, uom}, failed_qty: {qty, uom}}] }, reason: "...", source_refs: [...] }

7. Putaway to storage location
   → Scan bin barcode → resolve to bin_code
   → POST /ics/putaway/record { putaway: { variant_id, qty: {qty, uom:"EA"}, to_zone_code: "STOCKROOM", to_bin_code: "STOCKROOM-02" }, reason: "...", source_refs: [...] }

11.2 Process an Inbound Transfer

1. View pending inbound transfers
   → POST /ics/transfer/shipment/list { status: "shipped", limit: 20 }

2. Select shipment, scan each item
   → GET /pvm/barcode/resolve → variant_id
   → Match against shipment manifest

3. Record receipt (defaults to full receipt if received_lines omitted)
   → POST /ics/transfer/shipment/receive {
       shipment_id, expected_revision: 1, reason: "...", source_refs: [...]
     }
   → For partial receipt: include received_lines: [{ line_id, variant_id, qty: {qty, uom} }]

4. Flag exceptions with comments
   → POST /ics/comment { target_type: "transfer_shipment", target_id: shipment_id,
       body: "3 units arrived damaged, photos attached via MRS" }

11.3 Adjust Stock & Flag for Review

1. Scan product
   → GET /pvm/barcode/resolve → variant_id

2. Check current stock
   → POST /ics/stock/get { variant_id }

3. Create adjustment
   → POST /ics/adjustment/create {
       adjustment: { variant_id, delta_qty: { qty: -3, uom: "EA" }, reason_code: "INVENTORY.SHRINK",
         policy_versions: { adjustment_policy_version: "default" } },
       reason: "Shelf count 47, system 50", source_refs: [{ kind: "recount", id: "manual" }]
     }

4. Add review comment
   → POST /ics/comment { target_type: "stock", target_id: "v-...",
       body: "Shelf count 47, system shows 50. Adjusted -3. Needs investigation." }

11.4 Informative UI Pattern (design standard)

All screens in G3N/Inventory follow the informative UI pattern established by the Receiving page. This means:

  1. Human-readable identifiers — show vendor names, facility names, product codes; never raw GUIDs
  2. History-first — show existing state/history before the action form (e.g., comments shows existing comments above the compose form)
  3. Collapsible detail — list views are expandable; collapsed shows summary (counts, badges), expanded shows per-line detail
  4. Status color coding — consistent across all screens: submitted=yellow, approved=blue, shipped=purple, received/completed=green, rejected/cancelled=red
  5. Taxonomy reason codes — all reason inputs use the shared RECEIVING.* / INVENTORY.* taxonomy from reasonCode.ts; displayed as colored badges
  6. Over-limit warnings — flag unusual conditions (over-receipt, large adjustments) prominently but don't block
  7. Confirmation previews — before mutations, show a human-readable preview of what will happen
  8. Post-action refresh — after recording, refresh data to show updated state; show inline confirmation message

Shared infrastructure (in webapp/src/inventory/lib/):

ModulePurpose
format.tsfmtDate(), fmtTime(), fmtRelative() — consistent date formatting
reasonCodes.tsreasonInfo(code){label, color, bgColor}, reasonCodesFor(category), actionLabel(type)
nameResolver.tsloadMemberNames(org_guid) → bulk-load all org member display names via POST /ofm/member/names, resolveActorNames(guids[], org_guid?) → batch display names (triggers loadMemberNames on first call), resolveActorName(guid) → single name from cache, loadVendors() → vendor dropdown via PVM, resolveVendorName(ref), cacheFacilities(facilities[]) → populate facility cache from context, resolveFacilityName(guid) → caption, resolveFacilityCode(guid) → code, displayEntityId(entity, type), clearNameCaches() → call on logout. All caches are session-lifetime.

Shared components (in webapp/src/inventory/components/):

ComponentPurpose
HistoryListGeneric collapsible history list with expandable per-entry detail
StatusBadgeColor-coded status badge (used on transfers, product status, comments)

11.5 Screen-by-Screen Guide (complete functional + API reference)

This section describes every screen in G3N/Inventory from the merchant's perspective — what it does, who uses it, why it matters, and the exact API calls in order.

Base URL: https://api.g3nretailstack.comAll requests carry headers: x-session-guid, x-orgcode, x-logical-guid


Screen 1: Scan — "What is this item?"

What it is: The "what is this?" button. Staff picks up an item, scans it, instantly sees what it is and how much stock they have.

Who uses it: Everyone — cashiers checking if something's in stock for a customer, warehouse staff identifying unmarked items, managers doing spot checks.

What it does:

  • Camera-based barcode scan or manual entry (type a UPC/SKU)
  • Shows: product name, variant (color, size), image, barcode value
  • Shows: current stock levels at this facility
  • If the user has cost visibility permission, also shows cost data

Merchant value: "A customer asks if you have this shirt in Medium. Staff scans the Large on the shelf, sees the product, checks stock — Medium shows 3 available. No walking to a computer, no calling the back room."

Real-world use: 50+ times per day per store. The most-used screen by far. Takes <2 seconds from scan to answer.

API flow:

Step 1: Resolve barcode → product identity
  GET /pvm/barcode/resolve?orgcode={orgcode}&value={barcode}
  → { data: { barcode: { variant_id, variant_code }, owner: { variant_code } } }

  If not a GTIN barcode (manual SKU entry):
  GET /pvm/identifier/resolve?orgcode={orgcode}&type=sku&value={input}
  → { data: { identifier: { variant_id } } }

Step 2: Load product details
  GET /pvm/variant/get?orgcode={orgcode}&variant_id={variant_id}
  → { data: { variant_code, style_id, name, color, size, images } }

Step 3: Load stock at this facility
  POST /ics/stock/get
  { variant_id: "abc-123" }
  → { data: { on_hand: 47, available: 42, reserved: 3, committed: 2, in_transit: 5 } }

Screen 2: Stock Lookup — "Full stock history"

What it is: The deeper version of Scan. Not just "what is this" but "tell me everything about its stock history."

Who uses it: Warehouse managers investigating discrepancies, finance/audit staff reviewing cost movements, planners checking reorder points.

What it does:

  • Stock Levels tab: Full 9-bucket breakdown — on-hand, reserved, allocated, committed, available, in-transit, quarantine, damaged, consignment
  • Stock Card tab: Complete chronological ledger of every stock movement — receives, adjustments, transfers, picks, putaways, returns. Each entry shows: date, movement type (human-readable label), quantity change (+/-), reason code (colored badge), and running totals
  • Paginated — "Load more" for long histories

Merchant value: "Your physical count says 47 but the system says 52. Stock Lookup shows the full ledger — you see 5 units were adjusted out yesterday with reason 'Damage/Shrinkage' by Bob at 3pm. Mystery solved."

Real-world use: During cycle counts, discrepancy investigations, and when something doesn't add up. Maybe 5-10 times per day.

API flow:

Steps 1-3: Same as Scan (barcode resolve + variant get + stock get)

Step 4: Load stock card ledger (paginated)
  POST /ics/stock/card/list
  { variant_id: "abc-123", limit: 20 }
  → { data: { ledger: [
      { timestamp, movement_type: "receive", qty: +50, reason_code: "RECEIVING.OTHER", actor_ref: "user-guid" },
      { timestamp, movement_type: "adjustment", qty_delta: -3, reason_code: "INVENTORY.SHRINK" },
      ...
    ], next_token: "..." } }

Step 5: Load more (pagination)
  POST /ics/stock/card/list
  { variant_id: "abc-123", limit: 20, next_token: "eyJQSyI6..." }
  → next page of ledger entries

Screen 3: Receiving — "Goods arrived from vendor"

What it is: The "goods in" workflow. When a truck arrives with a purchase order, this is how stock enters the system.

Who uses it: Receiving clerks, warehouse managers, anyone processing inbound shipments.

What it does:

  • Shows all issued purchase orders waiting to be received (list with vendor name, date, PO code)
  • Tap a PO → see every line: product, ordered qty, already received, remaining
  • Enter receive qty and reject qty with reason code (Damaged, Defective, Wrong Item, Quality Issue, Expired, Packaging Issue, Short Shipment, Other)
  • Receipt history — collapsible timeline of previous receipts against this PO
  • Over-receipt warning — highlights in red but doesn't block
  • Submit → records PCM receipt + ICS inbound stock atomically

Merchant value: "A truck arrives with 200 boxes from Nike. The clerk opens Receiving, taps the Nike PO, counts boxes, enters quantities. 3 boxes are crushed — reject 3 with 'Damaged'. Submit. Inventory is instantly updated, vendor performance auto-calculates, and the buyer sees which items still haven't arrived."

Real-world use: Every delivery. 2-3 times per day (small store) to 20+ times per day (distribution center). This is where inventory accuracy starts.

API flow:

Step 1: Load purchase orders (page load — issued + partially received)
  POST /pcm/po/list
  { status: "issued", limit: 50 }
  → { data: { pos: [{ po_id, po_code: "PO-2026-0042", vendor_ref: { vendor_code: "NIKE" }, issued_at }] } }
  Also load partially received POs (can still receive remaining lines):
  POST /pcm/po/list
  { status: "partially_received", limit: 50 }

  Vendor names pre-loaded for display:
  GET /pvm/vendor
  { status: "active", limit: 256 }
  → { data: { vendors: [{ vendor_code: "NIKE", caption: "Nike Inc." }] } }

Step 2: User taps a PO → load detail + receipt history (parallel)
  POST /pcm/po/get                     POST /pcm/receipt/list
  { po_id: "po-guid" }                 { po_id: "po-guid", limit: 50 }
  → PO lines with ordered qtys          → Prior receipts with per-line breakdown

Step 3: User enters quantities and submits (two sequential calls)
  POST /pcm/receipt/record
  { receipt: {
      po_id, request_context: { session_guid, orgcode, actor, context_source, session_fingerprint, logical_guid },
      lines: [
        { po_line_id, variant_id, qty: { qty: 95, uom: "EACH" }, rejected_qty: 3, rejection_reason: "RECEIVING.DAMAGED" },
        { po_line_id, variant_id, qty: { qty: 50, uom: "EACH" } }
      ],
      policy_refs: { receipt_policy_version: "default" }
    },
    reason: "PO receipt via G3N/Inventory",
    source_refs: [{ kind: "po", id: "po-guid" }],
    idempotency_key: "pcm-receipt-po-guid-{uuid}"
  }

  → Response includes { data: { receipt: { receipt_id, status: "received", ... }, po_status: "received" | "partially_received" } }
  → The PO status is auto-updated server-side. Use po_status from the response to update the UI.

  Then immediately:
  POST /ics/receive/record
  { receipt: {
      request_context: { ... },
      lines: [{ variant_id, qty: { qty: 95, uom: "EACH" } }, ...],
      policy_versions: { receiving_policy_version: "default" }
    },
    reason: "PO receipt via G3N/Inventory",
    source_refs: [{ kind: "po", id: "po-guid" }],
    idempotency_key: "ics-receive-po-guid-{uuid}"
  }

Screen 4: Putaway — "Shelve received goods"

What it is: After goods are received, they need to go to a storage location. This records where each item was put.

Who uses it: Warehouse staff pushing carts of received goods to shelves, bins, and racks.

What it does:

  • Scan the item → shows product + current stock
  • Select zone (dropdown, e.g., "Main Floor", "Back Room") and bin (e.g., "Aisle 3 Shelf B")
  • Falls back to text input if zone/bin lists unavailable
  • Confirmation preview: "Putting away 5× SHIRT-BLU-M → MAIN FLOOR / AISLE-3-SHELF-B"
  • Submit → records putaway, refreshes stock

Merchant value: "When the picker needs to find that Medium Blue Shirt, they look up its bin location instead of searching the whole stockroom. Putaway is how items get their 'address' in the warehouse."

Real-world use: After every receiving session. 50-200 putaway records per receiving event.

API flow:

Step 1: Load zones (page mount)
  POST /ics/zone/list
  { limit: 50 }
  → { data: { zones: [
      { zone_code: "MAIN", caption: "Main Floor", bins: [{ bin_code: "A3-B", caption: "Aisle 3 Shelf B" }] },
      { zone_code: "BACK", caption: "Back Room", bins: [...] }
    ] } }

Step 2: Scan product (same barcode resolve + stock get as Scan)

Step 3: User selects zone/bin, enters qty, submits
  POST /ics/putaway/record
  { putaway: { variant_id, qty: { qty: 5, uom: "EACH" }, to_zone_code: "MAIN", to_bin_code: "A3-B" },
    reason: "Putaway via G3N/Inventory",
    source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
    idempotency_key: "putaway-{variant_id}-{uuid}"
  }

Step 4: Post-putaway stock refresh
  POST /ics/stock/get { variant_id }
  → updated stock position (confirms putaway recorded)

Screen 5: Transfers — "Move stock between stores"

What it is: Moving stock between stores/warehouses. Store A has too many, Store B needs more.

Who uses it: District managers, warehouse managers, inventory planners.

What it does:

  • Inbound tab: Transfers coming TO your facility. Approve or receive shipments.
  • Outbound tab: Transfers you sent. Track status.
  • + New tab: Create a new transfer request (destination, product, qty).
  • Status timeline: requested → approved → shipped → received
  • Facility names shown as human-readable captions (not GUIDs)

Merchant value: "Your Downtown store is sold out of the hot new hoodie but your Mall store has 30. Create a transfer, approve, ship, receive. Stock balances update at both locations automatically."

Real-world use: Multi-store merchants use this daily. Critical for seasonal rebalancing.

API flow:

Step 1: Load transfers (page load / tab switch)
  POST /ics/transfer/request/list
  { status: "approved", limit: 50 }   ← inbound tab
  POST /ics/transfer/request/list
  { limit: 50 }                        ← outbound tab

Step 2: User taps a transfer → load detail + shipments (parallel)
  POST /ics/transfer/request/get       POST /ics/transfer/shipment/list
  { transfer_id }                      { transfer_id, limit: 50 }

Step 3a: Approve (outbound, status=submitted)
  POST /ics/transfer/request/approve
  { transfer_id, expected_revision, reason: "Approved via G3N/Inventory", source_refs: [...] }

Step 3b: Receive shipment (inbound, status=shipped)
  POST /ics/transfer/shipment/receive
  { shipment_id, expected_revision, reason: "Received via G3N/Inventory",
    source_refs: [...], idempotency_key: "transfer-receive-{shipment_id}-{uuid}" }

Step 3c: Create new transfer
  POST /ics/transfer/request/create
  { transfer: {
      request_context: { ... },
      source_logical_guid: "your-facility",
      dest_logical_guid: "other-facility",
      lines: [{ variant_id, qty: 10 }],
      policy_refs: { transfer_policy_version: "default" }
    },
    reason: "Transfer request via G3N/Inventory", source_refs: [...]
  }

Screen 6: Adjustments — "Fix discrepancies"

What it is: Fixing inventory when the system doesn't match reality. Two modes: change quantity or change condition.

Who uses it: Warehouse managers, inventory controllers, loss prevention staff.

What it does:

  • Qty Adjust mode: Add or subtract stock with reason code (Cycle Count, Damage, Shrinkage, Found Stock, etc.)
  • Transition mode: Move stock between buckets (Available → Quarantine → Damaged) without changing total
  • Large-delta warning at ±100 units
  • Activity history shows recent ledger entries with actor display names, reason badges, zone/bin paths

Merchant value: "Physical count says 47 shirts, system says 52. Adjust -5 with reason 'Cycle Count'. Or: a customer returns a shirt with a stain. Transition 1 unit from Available to Damaged."

Real-world use: During cycle counts, after discovering theft/shrinkage, when items are damaged. 10-30 adjustments per day per store.

API flow:

Step 1: Scan product (barcode resolve + stock get + ledger load)
  POST /ics/stock/get { variant_id }
  POST /ics/stock/card/list { variant_id, limit: 15 }
  Actor names batch-resolved via: POST /ofm/member/names { org_guid } → returns all member display_names in one call (cached per session)

Step 2a: Qty Adjust mode
  POST /ics/adjustment/create
  { adjustment: {
      variant_id, delta_qty: { qty: -5, uom: "EACH" },
      reason_code: "INVENTORY.SHRINK",
      policy_versions: { adjustment_policy_version: "default" }
    },
    reason: "Cycle count variance",
    source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
    idempotency_key: "adjustment-{variant_id}-{uuid}"
  }

Step 2b: Transition mode
  POST /ics/stock/transition
  { transition: {
      variant_id, qty: { qty: 2, uom: "EACH" },
      from_bucket: "available", to_bucket: "damaged",
      reason_code: "INVENTORY.DAMAGE",
      policy_versions: { transition_policy_version: "default" }
    },
    reason: "Stained items found", source_refs: [...],
    idempotency_key: "transition-{variant_id}-{uuid}"
  }

Step 3: Post-action refresh (stock get + stock card list)

Screen 7: RTV (Return to Vendor) — "Send defective goods back"

What it is: Sending defective merchandise back to the vendor for credit or replacement.

Who uses it: Receiving clerks, buyers, warehouse managers.

What it does:

  • Scan defective product
  • Select vendor from dropdown (populated from PVM vendor list, falls back to text input)
  • Enter qty + reason (Damaged, Defective, Wrong Item, Quality Issue, Expired, etc.)
  • Confirmation preview in red: "Return 3× SHIRT-BLU-M → Nike Inc. (Damaged)"
  • Submit → creates PCM RTV record

Merchant value: "You received 100 jackets from North Face but 12 have broken zippers. Create an RTV for 12 units, reason 'Defective'. This feeds into the vendor performance scorecard — North Face's quality score drops."

Real-world use: During or after receiving. 1-5 RTVs per week per store.

API flow:

Step 1: Load vendors (page mount)
  GET /pvm/vendor
  { status: "active", limit: 256 }
  → { data: { vendors: [{ vendor_code: "NIKE", caption: "Nike Inc." }, ...] } }
  → Populates vendor dropdown. Falls back to text input if unavailable.

Step 2: Scan product (barcode resolve)

Step 3: User selects vendor, enters qty/reason, submits
  POST /pcm/rtv/create
  { rtv: {
      vendor_ref: { vendor_code: "NIKE" },
      request_context: { session_guid, orgcode, actor, context_source, session_fingerprint, logical_guid },
      lines: [{ variant_id, qty: 3, reason_code: "RECEIVING.DAMAGED", reason: "Broken zippers" }]
    },
    reason: "Broken zippers on 3 units",
    source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
    idempotency_key: "rtv-{variant_id}-{uuid}"
  }

Screen 8: Comments — "Flag for review"

What it is: Sticky notes on inventory items. Staff flags issues and communicates across shifts.

Who uses it: Everyone — cashiers noticing something wrong, warehouse staff flagging discrepancies, managers leaving instructions.

What it does:

  • Scan product → see existing comments first (history-first pattern)
  • Open count badge — how many unresolved flags (e.g., "3 open")
  • Each comment shows: text, author display name (resolved from OFM), relative time ("2h ago"), tag badges, status
  • 7 tags: Discrepancy, Damage, Shelf Life, Location Error, Quality Issue, Recount Needed, Other
  • Post new comment with optional tag

Merchant value: "Morning shift notices the system says 20 widgets but the shelf has only 15. They flag it: 'Discrepancy — shelf count 15, system 20. Please recount.' Afternoon shift sees the flag, does a recount, adjusts stock, resolves it."

Real-world use: The informal communication channel for inventory issues. 5-15 comments per day per store. Critical for multi-shift operations.

API flow:

Step 1: Scan product (barcode resolve)

Step 2: Load existing comments
  POST /ics/comment/list
  { target_type: "stock", target_id: "{variant_id}", limit: 30 }
  → { data: { comments: [
      { comment_id, body, author_guid, created_at, hashtags: ["discrepancy"], status: "current" },
      ...
    ] } }

Step 3: Resolve author names (single bulk call, cached per session)
  POST /ofm/member/names { org_guid: "{org_guid}" }
  → { data: { names: [{ user_guid: "...", display_name: "Jane Smith" }, ...] } }
  → One call returns all org member display names. Cached in nameResolver.

Step 4: Post new comment
  POST /ics/comment
  { target_type: "stock", target_id: "{variant_id}",
    body: "Found 5 units behind the shelf. Adjusting stock.",
    hashtags: ["recount"], caption: "Found 5 units behind the shelf..." }

Step 5: Reload comments (same as Step 2 — shows new comment in list)

Screen 9: Products — "Product encyclopedia"

What it is: Everything the system knows about a product, in one place.

Who uses it: Managers investigating products, buyers checking vendor/brand data, staff verifying barcodes.

What it does:

  • Details tab: Variant code, style code, color, size, status, kit type. All registered barcodes with scheme and primary badge.
  • Stock tab: Same 9-bucket stock display as Stock Lookup.
  • Activity tab: Same ledger as Stock Lookup (lazy-loaded on tab click).

Merchant value: "A new barcode label won't scan. The manager opens Product Lookup, searches by the old barcode, checks the Barcodes section — the new barcode isn't registered. Now they know to contact the buyer to add it."

Real-world use: Reference/investigation tool. 5-10 lookups per day.

API flow:

Step 1: Scan product (barcode resolve)

Step 2: Load variant + barcodes + stock (parallel)
  GET /pvm/variant/get?orgcode={orgcode}&variant_id={variant_id}
  → { data: { variant_code, style_code, color, size, status, kit_type } }

  GET /pvm/barcode/list?orgcode={orgcode}&variant_id={variant_id}
  → { data: { barcodes: [
      { value: "0123456789012", scheme: "ean-13", is_primary: true },
      { value: "7890123456789", scheme: "upc-a", is_primary: false }
    ] } }

  POST /ics/stock/get { variant_id }
  → stock position

Step 3: User clicks Activity tab → lazy-load ledger
  POST /ics/stock/card/list { variant_id, limit: 20 }
  → same ledger format as Stock Lookup

Who Uses What — Role Matrix

RolePrimary ScreensFrequency
Cashier / Sales staffScan, Comments50+ scans/day
Receiving clerkReceiving, RTV, PutawayEvery delivery
Warehouse staffPutaway, Scan, AdjustmentsAll day
Warehouse managerAll screensAll day
Inventory controllerStock Lookup, Adjustments, CommentsDuring counts
Buyer / ProcurementReceiving (monitor), RTV, ProductsWeekly
Store managerTransfers, Adjustments, Stock LookupAs needed
District managerTransfers (multi-store rebalancing)Weekly

The Flow of Goods

Vendor ships → [Receiving] → [Putaway] → shelf
                    ↓ (defective)
                  [RTV] → back to vendor

shelf → customer buys (POS) → stock decremented
shelf → damaged found → [Adjustments] → quarantine/damaged
shelf → wrong count → [Adjustments] → qty correction
shelf → needs to move → [Transfers] → another store

Any time → [Scan] to check what something is
Any time → [Stock Lookup] to see full history
Any time → [Comments] to flag an issue
Any time → [Products] to see product master data

API Summary per Screen

ScreenRead callsWrite callsServices hit
Scan30PVM, ICS
Stock Lookup4+0PVM, ICS
Receiving42PCM, ICS, PVM
Putaway41PVM, ICS
Transfers3-41ICS
Adjustments41PVM, ICS, OFM
RTV31PVM, PCM
Comments3+1PVM, ICS, OFM
Products4+0PVM, ICS

Every screen uses the shared scanner hook (PVM barcode/resolve → variant/get) as its entry point. Every mutation includes idempotency_key, reason, and source_refs for auditability. Every request carries session headers (x-session-guid, x-orgcode, x-logical-guid).


12. SDK Recommendations

Build a thin Kotlin SDK (g3n-inventory-sdk) that wraps these patterns:

12.1 Core Responsibilities

  • Header injection: Automatically add x-session-guid, x-orgcode, x-logical-guid to every request from stored context.
  • Idempotency key management: Generate and attach keys for mutations; store pending keys until confirmed.
  • Retry with backoff: On retryable errors (429, 5xx, network), retry up to 3 times with exponential backoff + jitter.
  • Session expiry handling: On 401, clear context and emit a "session expired" event for the UI to handle.
  • Barcode cache: LRU cache (500 entries, 5-min TTL) for barcode→variant mappings.
  • Typed models: Data classes for each request/response per endpoint.

12.2 Context Object

kotlin
data class G3nContext(
    val sessionGuid: String,
    val orgcode: String,
    val logicalGuid: String,
    val cccode: String? = null,
    val capabilities: Capabilities
)

Set once after login + org/facility selection. Pass to every SDK call.

12.3 Request Timeouts

OperationTimeout
Barcode resolve (PVM)3s
Stock lookup (ICS)5s
Receive/putaway/adjustment (ICS)10s
PO receipt (PCM)10s
Transfer operations (ICS)10s
Session create (USM)10s

13. Security Considerations

  • Never store passcode on device. Store only session_guid in Android Keystore.
  • Never log session_guid, x-api-key, or passcode values.
  • Always use HTTPS. The API Gateway enforces TLS 1.2+.
  • Anti-enumeration: Some endpoints return 404 even when the record exists (if the caller isn't associated with the org). Do not assume 404 means "does not exist."
  • Cost data: Only display cost fields if cost_view, ics_cost_admin, or finance_audit is in the user's roles. The server strips cost fields from responses for unauthorized users, but the app should also hide cost UI elements.

14. Development Environment

The dev-seed scripts create three pre-loaded orgs for testing against the live api.g3nretailstack.com stack.

OrgOrgcodeVerticalStylesVariantsFacilities
Aurora FashionAURORA3Fashion/apparel757985 (1 DC + 4 stores)
NexGen SportingNEXGEN3Sporting goods594964 (1 DC + 3 stores)
Shemew Pet SupplySHEMEW3Pet supply371953 (1 DC + 2 stores)

Test users: Three pre-created users with different role profiles. Credentials are provisioned by the dev-seed script (scripts/dev-seed/run.ts) — see the script source for current values.

EmailRolePurpose
owner@g3ntest.devOwner (all permissions, sees costs)Full access testing, cost visibility
warehouse@g3ntest.devWarehouse operatorPrimary app persona: receiving, adjustments, transfers
viewer@g3ntest.devRead-only (ics_view, pvm_view, pcm_view, ofm_view)Test role gating: no mutations, no cost visibility

Pre-seeded enrichment data (per org):

  • 5 stock comments with various hashtags
  • 5 adjustments (shrink, damage, adjust, FIFO, recall)
  • 2 stock transitions (available→damaged, available→quarantine)
  • 4 transfers in different states: pending, approved, in_transit, completed
  • 4-5 open POs (status: issued) for receiving practice
  • Zones and bins at all facilities (DC: 6 zones, stores: 4 zones, 3 bins each)

Sample barcodes for scanning tests: 3000000000014 (AURORA3), 3000000007990 (NEXGEN3).

Running the dev-seed:

bash
npx tsx scripts/dev-seed/run.ts          # Creates orgs, facilities, products, stock, zones, bins, transfers
npx tsx scripts/dev-seed/enrich.ts       # Adds comments, adjustments, transitions, multi-state transfers
npx tsx scripts/dev-seed/simulate.ts     # Runs full workflow simulation (PO → receive → putaway → transfer)

15. OpenAPI Specs

Full machine-readable contracts for code generation. Each service provides a downloadable YAML spec and an interactive API explorer:

ServiceOpenAPI YAMLInteractive Explorer
ICSopenapi.yamlExplorer
PCMopenapi.yamlExplorer
PVMopenapi.yamlExplorer
OFMopenapi.yamlExplorer
USMopenapi.yamlExplorer
UASopenapi.yamlExplorer
SCMopenapi.yamlExplorer

Use the YAML specs with OpenAPI code generators (e.g., openapi-generator for Kotlin) to produce typed request/response models. The interactive explorers (powered by Scalar) let you browse endpoints, schemas, and try requests directly.


16. Common References

TopicURL
Headers & identityhttps://doc.g3nretailstack.com/common/headers-identity.html
Error tagshttps://doc.g3nretailstack.com/common/error-tags.html
Idempotency & retrieshttps://doc.g3nretailstack.com/common/idempotency-retry.html
Rate limits & timeoutshttps://doc.g3nretailstack.com/common/rate-limit-timeouts.html
Performance SLOshttps://doc.g3nretailstack.com/common/performance-slos.html
Versioning & compatibilityhttps://doc.g3nretailstack.com/common/versioning-compat.html
Role matrixhttps://doc.g3nretailstack.com/common/role-matrix.html
SDK integration checklisthttps://doc.g3nretailstack.com/common/sdk-integration-checklist.html