{
 "info": {
  "name": "WTF Platform API v1",
  "description": "Complete API collection for the Workflow Traceability Framework.\n\nSetup:\n1. Set collection variable `baseUrl` to your API server (default: https://wtfapi.serviceproof.net)\n2. Set `apiKey` to your TENANT API key (get one from the Portal → Ops → API Keys). This is the collection default — every request inherits Bearer `{{apiKey}}`.\n3. Set `globalKey` to a GLOBAL-ADMIN API key (TenantId=0). The global-admin folders/requests override their auth to use `{{globalKey}}`: **Tenant Admin API**, **User API (provision/find-or-create)**, **Maintenance (purge-soft-deleted)**, and **Concierge route**. Everything else (entities, data, reports, forms, surfaces, assets, workflows, context, messaging send/read/turns/pin) is tenant-scoped and uses `{{apiKey}}`.\n\nAlternatively, use Login + SelectTenant flow to get a session token.\n\nSections:\n- Auth: Login, 2FA, tenant selection\n- Entity API: Full CRUD for entities and properties\n- Tenant Admin API: Tenant provisioning and management\n- Schema: Reference data and type tables\n\n**Wave 1 / Wave 2 / Wave 5 refresh (2026-04-25):** added multi-section save endpoints, expectedLayoutVersion examples, composite-key 422 demo, and the universal {ok, issues[]} envelope reference. Mojibake (—) corrected to proper em-dashes.",
  "_postman_id": "wtf-api-v1",
  "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 },
 "auth": {
  "type": "bearer",
  "bearer": [
   {
    "key": "token",
    "value": "{{apiKey}}",
    "type": "string"
   }
  ]
 },
 "variable": [
  {
   "key": "baseUrl",
   "value": "https://wtfapi.serviceproof.net",
   "type": "string"
  },
  {
   "key": "apiKey",
   "value": "YOUR_API_KEY_HERE",
   "type": "string"
  },
  {
   "key": "globalKey",
   "value": "YOUR_GLOBAL_ADMIN_API_KEY_HERE",
   "type": "string"
  },
  {
   "key": "entityId",
   "value": "",
   "type": "string"
  },
  {
   "key": "propertyId",
   "value": "",
   "type": "string"
  },
  {
   "key": "entityPropertyId",
   "value": "",
   "type": "string"
  },
  {
   "key": "tenantId",
   "value": "1",
   "type": "string"
  },
  {
   "key": "userId",
   "value": "",
   "type": "string"
  },
  {
   "key": "workflowId",
   "value": "",
   "type": "string"
  },
  {
   "key": "stepId",
   "value": "",
   "type": "string"
  },
  {
   "key": "eiRecordId",
   "value": "",
   "type": "string"
  },
  {
   "key": "viewId",
   "value": "",
   "type": "string"
  },
  {
   "key": "acmeServiceLocationEntityId",
   "value": "100011",
   "description": "Tenant-local EntityId for ServiceLocation (GlobalSourceId 117)."
  },
  {
   "key": "acmeEquipmentEntityId",
   "value": "100010",
   "description": "Tenant-local EntityId for Equipment (GlobalSourceId 116)."
  },
  {
   "key": "actionPackInstanceId",
   "value": "1",
   "type": "string"
  },
  {
   "key": "actionPackId",
   "value": "1",
   "type": "string"
  },
  {
   "key": "actionPackRunId",
   "value": "1",
   "type": "string"
  },
  {
   "key": "subscriptionId",
   "value": ""
  },
  {
   "key": "lastToken",
   "value": ""
  },
  {
   "key": "callbackUrl",
   "value": "https://webhook.site/REPLACE-WITH-YOUR-UUID"
  }
 ],
 "item": [
  {
   "name": "Auth",
   "description": "Authentication flow. Use Login → SelectTenant to get a bearer token, or use an API key directly.",
   "item": [
    {
     "name": "Login",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"userName\": \"kbarrett\",\n  \"password\": \"your-password\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/Login",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "Login"
       ]
      },
      "description": "Authenticate with credentials and optionally select a tenant. Returns a session token (8-hour TTL) after passing 2FA if configured. For integrations, use an API key instead."
     }
    },
    {
     "name": "SelectTenant",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/SelectTenant",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SelectTenant"
       ]
      },
      "description": "Pass the token from Login in the Authorization header. Returns a full bearer token for API calls."
     }
    }
   ]
  },
  {
   "name": "User API",
   "auth": {
    "type": "bearer",
    "bearer": [
     {
      "key": "token",
      "value": "{{globalKey}}",
      "type": "string"
     }
    ]
   },
   "description": "User lifecycle management (global users + global invites). Global admin (TenantId=0) for CRUD; find-or-create accepts any admin. Folder uses {{globalKey}} -- set your global-admin API key.",
   "item": [
    {
     "name": "Create User",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"userName\": \"jsmith\",\n  \"email\": \"john@acme.com\",\n  \"cellPhone\": \"+1-555-0100\",\n  \"languageRegionId\": 69,\n  \"timeZoneId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create platform user. Password auto-expired — forces invite reset."
     }
    },
    {
     "name": "Create User (German speaker)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"userName\": \"mmueller\",\n  \"email\": \"max@example.de\",\n  \"languageRegionId\": 50,\n  \"timeZoneId\": 5\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "User gets German emails. languageRegionId=50 is German (Germany)."
     }
    },
    {
     "name": "List Users",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "list"
       ]
      },
      "description": "All platform users with email, phone, lock status.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Get User Detail",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}"
       ]
      },
      "description": "Full detail: contacts, tenant memberships, lock status.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Update Language",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change user language to German. All future emails in German."
     }
    },
    {
     "name": "Update Timezone",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"timeZoneId\": 5\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change timezone. All timestamps displayed in this zone."
     }
    },
    {
     "name": "Update Username",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"userName\": \"john.smith\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change a global user's login name, language, or timezone preferences. Requires global admin (TenantId=0) with AdminUser role."
     }
    },
    {
     "name": "Add Email Contact",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}/contacts",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}",
        "contacts"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"contactTypeId\": 2,\n  \"data\": \"john.backup@gmail.com\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Type 1=Email, 2=RecoveryEmail, 3=CellPhone"
     }
    },
    {
     "name": "Add Phone Contact",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}/contacts",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}",
        "contacts"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"contactTypeId\": 3,\n  \"data\": \"+1-555-0200\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Add a backup email or phone number to a user's contact list for 2FA and account recovery. Globally unique email addresses cannot be reused across users."
     }
    },
    {
     "name": "Lock User",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}/lock",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}",
        "lock"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"lockReasonId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Locks globally. Reason: 1=Admin, 2=Security, 3=Policy"
     }
    },
    {
     "name": "Unlock User",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/{{userId}}/unlock",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "{{userId}}",
        "unlock"
       ]
      },
      "description": "No body needed."
     }
    },
    {
     "name": "Find-or-Create (new user)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/find-or-create",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "find-or-create"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"email\": \"maria@salspizza.com\",\n  \"firstName\": \"Maria\",\n  \"lastName\": \"Garcia\",\n  \"cellPhone\": \"+1-555-0300\",\n  \"languageRegionId\": 79,\n  \"timeZoneId\": 1,\n  \"tenantId\": 5,\n  \"roleFlag\": 9216,\n  \"sendInvite\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Smart: find by email, create if needed, add to tenant, send invite in Spanish."
     }
    },
    {
     "name": "Find-or-Create (existing user)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/find-or-create",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "find-or-create"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"email\": \"john@acme.com\",\n  \"tenantId\": 3,\n  \"roleFlag\": 60416,\n  \"sendInvite\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "User exists — just adds to tenant with Power User roles and sends invite."
     }
    },
    {
     "name": "Find-or-Create (no tenant)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/users/find-or-create",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "users",
        "find-or-create"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"email\": \"new.person@example.com\",\n  \"languageRegionId\": 106,\n  \"timeZoneId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create user only (French). No tenant assignment. Add to tenant later."
     }
    }
   ]
  },
  {
   "name": "Schema & Reference",
   "description": "Static reference data — data types, categories, verticals, and all type tables. Also: whoami for caller self-discovery (timezone, locale, identity).",
   "item": [
    {
     "name": "WhoAmI",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema/whoami",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema",
        "whoami"
       ]
      },
      "description": "Returns the authenticated caller's identity + preferences + server clock in a single call. No role gate — the bearer token IS the gate. Use this to:\n\n• Auto-discover your TenantId / TenantUserId / UserId without a separate admin lookup\n• Learn the caller's configured TimeZoneId (IANA name + current UTC-offset minutes, DST-aware)\n• Read the caller's LanguageRegionId for localized content\n• Clock-sync against serverTimeUtc\n\nResponse shape:\n```\n{\n  \"tenantId\": 42,\n  \"tenantUserId\": 1337,\n  \"userId\": 1001,\n  \"userName\": \"kirk@cimbrian.com\",\n  \"displayName\": \"Kirk Barrett\",\n  \"roleFlag\": 2047,\n  \"languageRegionId\": 7,\n  \"timeZoneId\": 24,\n  \"timeZoneIana\": \"Europe/Berlin\",\n  \"timeZoneAbbrev\": \"CET\",\n  \"standardOffsetMinutes\": 60,\n  \"currentOffsetMinutes\": 120,\n  \"serverTimeUtc\": \"2026-04-20T15:42:00Z\",\n  \"serverTimeLocal\": \"2026-04-20T17:42:00\"\n}\n```\n\nDate handling convention: every `datetime2` column returned by the API is UTC and is emitted with a trailing `Z`. Use `currentOffsetMinutes` from this endpoint when you need to localize UTC timestamps to the caller's zone.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "GET Languages",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/languages",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "languages"
       ]
      },
      "description": "Returns the tenant's configured languages with LanguageRegionId, LanguageCode, and DisplayName. Use LanguageRegionId values in ?languageRegionId= query params and PUT /display calls.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "GET Schema (all)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema"
       ]
      },
      "description": "Returns all 14 static type tables in one call. No DB queries — instant response.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "GET Schema (filtered)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema?section=dataTypes,verticals,categories",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema"
       ],
       "query": [
        {
         "key": "section",
         "value": "dataTypes,verticals,categories"
        }
       ]
      },
      "description": "Filter to specific sections. Available: dataTypes, categories, verticals, stepTypes, actionTypes, actionTriggers, roles, contactTypes, mediaRequirements, binaryInputSources, metaKeys, lockReasons, syncDirections, syncJobStatuses",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "GET Verticals",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/verticals",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "verticals"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List all industry verticals (General, Field Service, Healthcare, Insurance, Logistics, Food Service). Used to catalog and filter importable templates."
     }
    },
    {
     "name": "GET Categories",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/categories",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "categories"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Discover the property category taxonomy for grouping fields in UI builders and schema templates."
     }
    },
    {
     "name": "GET Catalog (browse templates)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/catalog?verticalId=0",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "catalog"
       ],
       "query": [
        {
         "key": "verticalId",
         "value": "0",
         "description": "0=all, 2=FieldService, 6=FoodService"
        }
       ]
      },
      "description": "Browse global catalog templates available for import. Filtered by vertical.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    }
   ]
  },
  {
   "name": "Properties",
   "description": "Property CRUD — create, list, detail, update, delete. Properties are atomic data fields (Text, Number, Choice, etc.).",
   "item": [
    {
     "name": "Create Text (with displays)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"FullName\",\n  \"dataTypeId\": 5,\n  \"categoryId\": 1,\n  \"maxLength\": 128,\n  \"minLength\": 1,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Full Name\",\n      \"description\": \"First and last name\"\n    },\n    {\n      \"languageRegionId\": 50,\n      \"name\": \"Vollstaendiger Name\",\n      \"description\": \"Vor- und Nachname\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a Property with `dataTypeId = 5` (Text). The displays array seeds the per-language Name / Description rows in one shot. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Create Number",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Age\",\n  \"dataTypeId\": 1,\n  \"categoryId\": 1,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Age\",\n      \"description\": \"Years old\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a numeric property with optional min/max/decimal precision constraints. Can be reused across multiple entities."
     }
    },
    {
     "name": "Create DateTime",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"StartDate\",\n  \"dataTypeId\": 2,\n  \"categoryId\": 6,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Start Date\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a date-time property. Stores UTC timestamps with millisecond precision; always emitted as ISO 8601 with Z suffix."
     }
    },
    {
     "name": "Create Currency",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Price\",\n  \"dataTypeId\": 11,\n  \"categoryId\": 5,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Price\",\n      \"description\": \"Unit price\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a monetary value property with configurable decimal places. Inherits tenant currency from settings."
     }
    },
    {
     "name": "Create Choice (with options)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Status\",\n  \"dataTypeId\": 6,\n  \"options\": [\n    \"Active\",\n    \"Inactive\",\n    \"Pending\",\n    \"Archived\"\n  ],\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Status\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a Property with `dataTypeId = 6` (Choice). The body options array becomes the fixed enumeration users can pick from in Forms / Data Studio / workflow Choice steps. Choice values write the option InternalName (not display text) - branch routing + filters compare against that. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Create Sensitive (with regex)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"SSN\",\n  \"dataTypeId\": 5,\n  \"isSensitive\": true,\n  \"maxLength\": 11,\n  \"regEx\": \"^\\\\d{3}-\\\\d{2}-\\\\d{4}$\",\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"SSN\",\n      \"description\": \"Social Security Number\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a sensitive Property (`isSensitive: true`). Sensitive values get HMAC-signed URLs on every render and never proxy through unsigned routes. `regex` enforces format on save client-side AND server-side via the PropertyValidator. The platform stays SOC2-clean by construction. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Create DateOnly",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"BirthDate\",\n  \"dataTypeId\": 3,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Date of Birth\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a date-only property (no time component). Useful for birthdays, policy dates, without time semantics."
     }
    },
    {
     "name": "Create Binary (toggle)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"IsActive\",\n  \"dataTypeId\": 10,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Active\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a true/false toggle property. Cannot be marked as required in catalog (forms can enforce required at layout boundary)."
     }
    },
    {
     "name": "List Properties",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "list"
       ]
      },
      "description": "Browse the tenant Properties catalog (paged, filterable). Each row carries DataType + IsSensitive + IsFullyTranslated + the displays for the requested language. Properties are Pillar 1 - the typed fields Entities compose from. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Get Property Detail",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}"
       ]
      },
      "description": "Fetch one Property + every language display + Choice options. Use to load a Property into an editor or to read its DataType / IsSensitive / regex / Min/Max / MaxDecimalPlaces constraints into a downstream validator. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Update Property (constraints)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"maxLength\": 256,\n  \"displayName\": \"Full Legal Name\",\n  \"displayDescription\": \"Complete legal name\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "PUT-merge a Property metadata or constraints. Updating IsSensitive is a one-way promotion - once a Property is sensitive, downgrading it is blocked (any rendered URL history would leak). Regex / Min/Max / MaxDecimalPlaces apply prospectively to new captures. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Update Property (category)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"categoryId\": 3\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update a property's category, constraints, or sensitivity flag. Linked properties (from global catalog) cannot have structural changes."
     }
    },
    {
     "name": "Set Display (German, locked)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}/display",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}",
        "display"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 50,\n  \"name\": \"Vollstaendiger Name\",\n  \"description\": \"Vor- und Nachname\",\n  \"isTranslationLocked\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Upsert the language-specific display for a Property. `isLocked` freezes the row so the auto-translator skips it on future sweeps (use when the human-written display is authoritative). Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Set Display (Spanish)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}/display",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}",
        "display"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 79,\n  \"name\": \"Nombre Completo\",\n  \"description\": \"Nombre y apellido\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Set the Spanish translation for a property's name and description. Use `isTranslationLocked: true` to prevent auto-translation from overwriting it."
     }
    },
    {
     "name": "Translate Property",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}/translate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}",
        "translate"
       ]
      },
      "description": "Fire the Azure-Translator pass for one Property across the tenant configured languages. Skips rows where `isLocked = true`. The same translator drives bulk passes via the Tenant Admin API. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Delete Property",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/{{propertyId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "{{propertyId}}"
       ]
      },
      "description": "Delete a property definition. Fails with 409 if still attached to any entities — remove it from all entities first."
     }
    }
   ]
  },
  {
   "name": "Entities",
   "description": "Entity CRUD — create, list, detail, update, delete. Entities are real-world objects (Customer, Vehicle, Order).",
   "item": [
    {
     "name": "Create Entity (with displays)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Customer\",\n  \"categoryId\": 1,\n  \"verticalId\": 0,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Customer\",\n      \"description\": \"A customer record\"\n    },\n    {\n      \"languageRegionId\": 50,\n      \"name\": \"Kunde\",\n      \"description\": \"Ein Kundendatensatz\"\n    },\n    {\n      \"languageRegionId\": 79,\n      \"name\": \"Cliente\",\n      \"description\": \"Un registro de cliente\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create an Entity + every language display in one call. The Tenant configured languages flow through; missing language rows fall back to InternalName at render. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Create Entity (minimal)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Invoice\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create an Entity (Pillar 2) - a typed record that composes Properties. The minimal body just supplies InternalName + an English display. EntityComposition is added separately via /entity-properties. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "List Entities",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "list"
       ]
      },
      "description": "Browse the tenant Entities (paged). Each row carries display Name + Description in the requested language + the property count. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "List Entities (German default)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "list"
       ],
       "query": [
        {
         "key": "languageRegionId",
         "value": "50"
        }
       ]
      },
      "description": "Same as List Entities, but the response prefers German displays (per the Tenant default language). The language-fallback chain is requested-language -> tenant-default -> InternalName, so a missing German row falls back automatically. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Get Entity Detail",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer"
       ]
      },
      "description": "Fetch one Entity + every language display + its EntityProperty composition (which Properties belong, with key-position + required + sensitive). Use to load an Entity into the schema editor or to drive a form/view generator. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Update Entity (metadata + displays)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Customer\",\n  \"categoryId\": 1,\n  \"verticalId\": 1,\n  \"name\": \"Customer\",\n  \"description\": \"Updated description demo from Postman\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update Customer entity metadata. PUT requires the full record (we don't use PATCH semantics). Preserves internalName so the rename-as-new flow doesn't trigger."
     }
    },
    {
     "name": "Update Entity (category only)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Customer\",\n  \"categoryId\": 2,\n  \"verticalId\": 1,\n  \"name\": \"Customer\",\n  \"description\": \"Moved to category 2 (demo)\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update just the categoryId on the Customer entity. Demonstrates that PUT requires a full record — send every field back, change only what you need."
     }
    },
    {
     "name": "Delete Entity",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/DemoEntityWontExist",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "DemoEntityWontExist"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "DELETE an entity by InternalName or numeric id. `DemoEntityWontExist` is intentional — expects 404 safely. To actually delete, replace with the InternalName of an entity you created and know is unused."
     }
    }
   ]
  },
  {
   "name": "Entity Composition",
   "description": "Attach properties and child entities to build data models. Control cardinality (required/optional, single/list).",
   "item": [
    {
     "name": "Attach Property (required, single)",
     "event": [
      {
       "listen": "test",
       "script": {
        "exec": [
         "var json = pm.response.json();",
         "if (json.entityPropertyId) pm.collectionVariables.set('entityPropertyId', json.entityPropertyId);"
        ]
       }
      }
     ],
     "request": {
      "method": "POST",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyId\": 100020,\n  \"isRequired\": true,\n  \"isKey\": false,\n  \"minCount\": 1,\n  \"maxCount\": 1,\n  \"position\": 99\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties"
       ]
      },
      "description": "Attach property 100020 (Customer's CustomerNumber — replace with your target PropertyId) to Customer as required single-value. propertyId here is a GLOBAL PropertyId from /api/v1/properties/list, not an EntityPropertyId. The server returns the created EntityPropertyId in the response."
     }
    },
    {
     "name": "Attach Property (optional, list)",
     "request": {
      "method": "POST",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyId\": 100022,\n  \"isRequired\": false,\n  \"isKey\": false,\n  \"minCount\": 0,\n  \"maxCount\": 99,\n  \"position\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties"
       ]
      },
      "description": "Attach property 100022 as an optional multi-value (list) field with up to 99 entries. maxCount=99 + minCount=0 = 'list of up to 99 optional values'. Replace 100022 with a real PropertyId from /api/v1/properties/list."
     }
    },
    {
     "name": "Attach Composite-Key Property (keyPosition)",
     "request": {
      "method": "POST",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyId\": 100020,\n  \"isKey\": true,\n  \"keyPosition\": 1,\n  \"isRequired\": true,\n  \"minCount\": 1,\n  \"maxCount\": 1,\n  \"position\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties"
       ]
      },
      "description": "Define a COMPOSITE-KEY column over the API: `isKey: true` + `keyPosition` (1/2/3 = the natural-key slot). This is how an integrator/agent builds a keyed entity the Context Engine + auto-recommender can anchor on. Omitting `keyPosition` on a key defaults the slot to `position`. On UPDATE (`PUT .../properties/{epId}`), `keyPosition` is PRESERVED when omitted — it won't wipe the slot; pass it explicitly to change the slot. Pass keyPosition 2 / 3 on additional properties for a 2- or 3-part composite key (e.g. ServiceLocation = CustomerNumber@1 + LocationNumber@2)."
     }
    },
    {
     "name": "Add Property Options (existing Choice)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"options\": [\n    \"Draft\",\n    \"Collecting\",\n    \"Submitted\",\n    \"Preparing\",\n    \"OutForDelivery\"\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/properties/OrderStatus/options",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "properties",
        "OrderStatus",
        "options"
       ]
      },
      "description": "Add Choice options to an EXISTING property (POST /api/v1/properties takes options only on create). Idempotent: existing InternalNames are skipped, new ones append after the max position. Choice (dataTypeId 6) only. BuilderAdmin."
     }
    },
    {
     "name": "Update Cardinality",
     "request": {
      "method": "PUT",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyId\": 100020,\n  \"isRequired\": true,\n  \"isKey\": false,\n  \"minCount\": 1,\n  \"maxCount\": 1,\n  \"position\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties/100046",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties",
        "100046"
       ]
      },
      "description": "Update an existing EntityProperty (the relationship row, identified by EntityPropertyId=100046). Send the full record — PUT replaces. Swap 100046 with an EntityPropertyId from /api/v1/entities/Customer."
     }
    },
    {
     "name": "Reorder Property",
     "request": {
      "method": "PUT",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyId\": 100020,\n  \"isRequired\": true,\n  \"isKey\": false,\n  \"minCount\": 1,\n  \"maxCount\": 1,\n  \"position\": 5\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties/100046",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties",
        "100046"
       ]
      },
      "description": "Reorder a property by changing its `position` field. Full-record PUT like all updates. Lower position = higher in the UI list."
     }
    },
    {
     "name": "Detach Property",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/properties/99999",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "properties",
        "99999"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "DELETE the attachment between Customer and EntityPropertyId=99999. The 99999 is intentional — expects 404 safely. To actually detach, use a real EntityPropertyId from /api/v1/entities/Customer (properties[].entityPropertyId)."
     }
    },
    {
     "name": "Attach Child Entity (1:many)",
     "request": {
      "method": "POST",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"childEntityId\": 100011,\n  \"isRequired\": false,\n  \"minCount\": 0,\n  \"maxCount\": 99,\n  \"position\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/children",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "children"
       ]
      },
      "description": "Attach ServiceLocation (EntityId=100011) as a 1:many child of Customer. Creates an EntityProperty row with ChildEntityId set — composition, not a scalar property. Replace 100011 with your target child EntityId."
     }
    },
    {
     "name": "Translate Entity Properties",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/translate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "translate"
       ]
      },
      "description": "Auto-translate Customer's property display names + descriptions into LanguageRegionIds 2 and 3. Skips already-translated rows unless overwrite=true. Uses Azure Translator under the hood — requires tenant's AzureTranslator* secrets set.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionIds\": [\n    2,\n    3\n  ],\n  \"overwrite\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    }
   ]
  },
  {
   "name": "Import",
   "description": "Import entities and properties from the global catalog into your tenant.",
   "item": [
    {
     "name": "Import from Catalog",
     "request": {
      "method": "POST",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entityIds\": [100, 101, 102],\n  \"propertyIds\": []\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/import",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "import"
       ]
      },
      "description": "Import global catalog templates. Entity IDs from GET /api/v1/catalog. Automatically imports linked properties and display data."
     }
    }
   ]
  },
  {
   "name": "Tenant Admin API",
   "auth": {
    "type": "bearer",
    "bearer": [
     {
      "key": "token",
      "value": "{{globalKey}}",
      "type": "string"
     }
    ]
   },
   "description": "Tenant provisioning and management (setting up a tenant). Requires global admin (TenantId=0) + AdminUser role. Folder uses {{globalKey}} -- set your global-admin API key.",
   "item": [
    {
     "name": "Create Tenant",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"name\": \"New Tenant\",\n  \"subDomain\": \"newtenant\",\n  \"host\": \"newtenant.localhost\",\n  \"verticalId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Provision a new tenant in one call: clones settings + languages from a template tenant, seeds storage sources, adds system admin and caller as users. Global admin only (TenantId=0)."
     }
    },
    {
     "name": "List Tenants",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve all tenants with user counts and activation status. Global admin only."
     }
    },
    {
     "name": "Get Tenant",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Fetch full tenant detail including settings JSON and CSS theme. Global admin only."
     }
    },
    {
     "name": "Update Tenant",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"name\": \"Updated Name\",\n  \"verticalId\": 2\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update tenant name, subdomain, description, or theme colors. Global admin only."
     }
    },
    {
     "name": "Suspend Tenant",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/suspend",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "suspend"
       ]
      },
      "description": "Lock a tenant — users cannot log in. Useful for pausing a customer temporarily. Global admin only."
     }
    },
    {
     "name": "Activate Tenant",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/activate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "activate"
       ]
      },
      "description": "Reactivate a previously suspended tenant so users can log in again. Global admin only."
     }
    },
    {
     "name": "List Users",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/users/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "users",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List all users in a specific tenant with role flags and lock status. Tenant admin required."
     }
    },
    {
     "name": "Add User",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/users",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "users"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"userId\": 2,\n  \"roleFlag\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Add a global user to a tenant with specified role flags. Cannot escalate roles you don't possess yourself."
     }
    },
    {
     "name": "Update Roles",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/users/2/roles",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "users",
        "2",
        "roles"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"roleFlag\": 255\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change a user's role flags within a tenant. Cannot grant roles you don't hold."
     }
    },
    {
     "name": "Remove User",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/users/2",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "users",
        "2"
       ]
      },
      "description": "Remove a user from a specific tenant. Does not delete the global user, just the tenant membership."
     }
    },
    {
     "name": "Send Invite",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/invite",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "invite"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"email\": \"newuser@example.com\",\n  \"roleFlag\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Send a branded invite email with a 7-day accept link. Email is sent in the user's configured language. Tenant admin required."
     }
    },
    {
     "name": "List Languages",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/languages/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "languages",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Discover which languages are enabled for a tenant and which is the default. Tenant admin required."
     }
    },
    {
     "name": "Add Language",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/languages",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "languages"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 50,\n  \"isDefault\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Enable a new language for a tenant or change the default language. Ensures at least one default language remains active."
     }
    },
    {
     "name": "List Email Templates",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/email-templates/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "email-templates",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve global and tenant-scoped email templates. Tenant templates can be customized for branding. Tenant admin required."
     }
    },
    {
     "name": "Clone Email Template",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/email-templates/clone",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "email-templates",
        "clone"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"emailTemplateId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Copy a global base template to your tenant so you can customize the subject, heading, or body HTML. Tenant admin required."
     }
    },
    {
     "name": "Save Email Template",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/email-templates/UserInvite",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "email-templates",
        "UserInvite"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageCode\": \"en\",\n  \"subject\": \"Your verification code is [[Code]]\",\n  \"htmlBody\": \"<html><body><h1>Verification</h1><p>Use code [[Code]].</p></body></html>\",\n  \"plainBody\": \"Verification: use code [[Code]].\",\n  \"isActive\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save (insert OR update) a tenant-specific email template for the given templateType. First save for a (tenantId, templateType, languageCode) triple inserts; subsequent calls with the same triple update. To start from the global default, call /email-templates/clone first, then edit. Body must include subject + htmlBody."
     }
    },
    {
     "name": "Get Tenant Branding",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/branding",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "branding"
       ]
      },
      "description": "Return the tenant's current branding JSON blob (BrandName + LogoKey + FaviconKey + AppleIconKey + AndroidIconKey + CSS). Same shape mobile + Portal read on every login."
     }
    },
    {
     "name": "Update Tenant Branding CSS",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/branding/css",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "branding",
        "css"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"css\": \"{\\\"BrandName\\\":\\\"Acme Field Services\\\",\\\"LogoKey\\\":\\\"acme-logo\\\",\\\"FaviconKey\\\":\\\"acme-favicon\\\",\\\"AppleIconKey\\\":\\\"acme-apple\\\",\\\"AndroidIconKey\\\":\\\"acme-android\\\",\\\"CSS\\\":\\\":root { --theme-primary: #1e6091; }\\\"}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Replace the tenant's brand JSON blob. The 'css' field carries the full branding structure as a JSON string (note: field is named CSS historically; carries BrandName + image keys + actual CSS overrides for --theme-* variables)."
     }
    },
    {
     "name": "Upload Branding Image (URL)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/branding/images",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "branding",
        "images"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"imageUrl\": \"https://your-cdn.com/brand-logo.png\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Upload + auto-process a brand logo. Pass either imageData (base64 + mimeType) OR imageUrl. Server downscales into Logo + Favicon32 + Favicon180 + Favicon192 variants and returns asset keys for each. 10 MB max."
     }
    },
    {
     "name": "Send Email",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/tenants/{{tenantId}}/send-email",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "tenants",
        "{{tenantId}}",
        "send-email"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"subject\": \"Test\",\n  \"htmlBody\": \"<p>Hello</p>\",\n  \"recipients\": [\n    {\n      \"email\": \"test@example.com\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Send a branded email to one or more recipients using a template wrapper and token substitution. Max 99 recipients per request."
     }
    }
   ]
  },
  {
   "name": "Asset API",
   "description": "Binary asset management (FileView/FileUpload/FileEdit roles).\n\n**The whole binary contract in one place (read this and you know everything):**\n\n1. UPLOAD — POST /api/v1/assets (multipart `file` + optional `title`, `description`, `storageShortcutId`, `sensitive=true`). Returns `{ assetKey }` (a GUID). The platform NEVER takes base64 in JSON and you never upload direct-to-storage.\n2. REFERENCE — put the key in your instance/form save: `{ \"Dataplate\": { \"key\": \"<assetKey>\", \"name\": \"plate.jpg\" } }`.\n3. READ (the easy part) — every entity/form/report response auto-resolves each binary field into a ready object: `{ key, name, url, directUrl?, mimeType, sensitive, sourceName, fileSize }`.\n   - `url` is ABSOLUTE (FQDN) — drop straight into <img>/<a>/<video>, even cross-origin. SENSITIVE assets arrive PRE-SIGNED (?sig=&exp=&u=, 15-min) so they render on a plain <img> with no Authorization header.\n   - `directUrl` only for non-sensitive public-CDN sources.\n   - Classify the element by `mimeType`, NEVER the URL (key-proxy URLs are extensionless by design).\n4. DOWNLOAD the bytes — GET the `url`, or GET /api/v1/assets/{key}/download (Bearer OR signed query).\n5. INTELLIGENCE on one asset — POST /api/v1/assets/{key} returns full detail (mime, size, source, folder, sensitivity, timestamps) by AssetKey alone.\n\nOne contract, every storage provider (Azure Blob / S3 / Google Drive / SFTP / local) — no SDK, no provider-specific code. See 'Get Asset', 'Download Asset', 'Upload Asset', and Entity Data > 'Get Instance — RESOLVED BINARY'.",
   "item": [
    {
     "name": "List Assets",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "list"
       ],
       "query": [
        {
         "key": "page",
         "value": "1"
        },
        {
         "key": "pageSize",
         "value": "25"
        }
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve all assets with optional filtering by source, folder, or search text. Includes folder path and metadata for each asset."
     }
    },
    {
     "name": "Search Assets",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/search/query",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "search",
        "query"
       ],
       "query": [
        {
         "key": "q",
         "value": "invoice"
        },
        {
         "key": "page",
         "value": "1"
        }
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Intelligent search with smart filters (type, folder name, size range, date range, sensitivity). Searches across all folders regardless of folder filter."
     }
    },
    {
     "name": "Get Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/00000000-0000-0000-0000-000000000000",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "00000000-0000-0000-0000-000000000000"
       ]
      },
      "description": "Replace GUID with actual asset key",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Download Asset (by key, FQDN)",
     "request": {
      "method": "GET",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/00000000-0000-0000-0000-000000000000/download",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "00000000-0000-0000-0000-000000000000",
        "download"
       ]
      },
      "description": "Stream the raw bytes with the correct Content-Type. Accepts EITHER the Bearer token OR a signed query (?sig=&exp=&u=&t=), so it also works on a plain <img src> / <a href>. NOTE: every entity/form/report read already returns each binary field resolved to an ABSOLUTE FQDN `url` (and `directUrl` when the source is public) - drop those straight into an element. This endpoint is the explicit by-key download when you want the bytes."
     }
    },
    {
     "name": "Upload Asset",
     "request": {
      "method": "POST",
      "body": {
       "mode": "formdata",
       "formdata": [
        {
         "key": "file",
         "type": "file",
         "src": ""
        },
        {
         "key": "title",
         "value": "Left Front Corner",
         "type": "text",
         "description": "Display name for the asset. If omitted, the original filename is used. The runner sets this to the step/slot caption so field-captured assets are meaningful, not cryptic camera filenames."
        },
        {
         "key": "description",
         "value": "Left Front Corner - Vehicle Intake - front corner photos",
         "type": "text",
         "description": "Free-text description stored on the asset (e.g. label + step name + step description)."
        },
        {
         "key": "storageShortcutId",
         "value": "",
         "type": "text",
         "description": "Optional. The canonical 'where binaries go' pointer (source + folder + path + sensitivity in one id). Omit to use the tenant default source."
        },
        {
         "key": "sensitive",
         "value": "",
         "type": "text",
         "description": "Optional 'true' to route the asset to the default SENSITIVE source (reads then require a signed URL)."
        },
        {
         "key": "folderId",
         "value": "",
         "type": "text"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets"
       ]
      },
      "description": "Upload a file as multipart/form-data. Returns `{ assetKey }` — a GUID you reference in instance/form record saves as `{ \"Field\": { \"key\": \"<assetKey>\", \"name\": \"...\" } }`. `title`/`description` are stored on the asset (the real upload filename is kept separately as OriginalFileName). Falls back to the tenant default source if no storageShortcutId/sensitive is given. On a later READ the field comes back fully resolved with an absolute FQDN `url` — see Entity Data > 'Get Instance — RESOLVED BINARY'."
     }
    },
    {
     "name": "Upload Asset (via Storage Shortcut)",
     "request": {
      "method": "POST",
      "body": {
       "mode": "formdata",
       "formdata": [
        {
         "key": "file",
         "type": "file",
         "src": ""
        },
        {
         "key": "storageShortcutId",
         "value": "100001",
         "type": "text",
         "description": "Discover via /api/v1/assets/shortcuts/list. Encapsulates source + folder + path + sensitivity in one ID. Explicit sourceId / folderId / subPath form fields override the shortcut's defaults at field level."
        },
        {
         "key": "title",
         "value": "Site Photo",
         "type": "text"
        }
       ]
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets"
       ]
      },
      "description": "Recommended upload pattern: target a Storage Shortcut by ID. The server resolves source + folder + path from the shortcut so your integration code stays insulated from admins reorganizing folders. Every TenantStorageSource has an auto-created '{SourceName} (Default)' shortcut you can fall back to, plus any custom shortcuts admins add in Storage Explorer."
     }
    },
    {
     "name": "Update Asset",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/00000000-0000-0000-0000-000000000000",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "00000000-0000-0000-0000-000000000000"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"displayName\": \"Updated Name\",\n  \"description\": \"Updated description\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update an asset's title, description, or metadata. BuilderAdmin scope required."
     }
    },
    {
     "name": "Delete Asset",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/00000000-0000-0000-0000-000000000000",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "00000000-0000-0000-0000-000000000000"
       ]
      },
      "description": "Remove asset metadata from the catalog. Pass `deleteFromStorage=true` to also delete the physical file from the storage provider."
     }
    },
    {
     "name": "Get Metadata",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/metadata/query",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "metadata",
        "query"
       ],
       "query": [
        {
         "key": "keys",
         "value": "00000000-0000-0000-0000-000000000000"
        }
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve rich metadata for one or more assets (MIME type, file size, folder path, source details)."
     }
    },
    {
     "name": "Update Metadata",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/metadata",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "metadata"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "[\n  {\n    \"assetKey\": \"00000000-0000-0000-0000-000000000000\",\n    \"properties\": {}\n  }\n]",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Bulk update titles and descriptions across multiple assets in one call."
     }
    },
    {
     "name": "List Sources",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sources",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sources"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Discover configured storage sources (Azure Blob, S3, FTP, etc.) without exposing credentials. Used to pick upload targets."
     }
    },
    {
     "name": "List Folders",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/folders/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "folders",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve virtual folder tree as a flat list with parent IDs. Build nested structure client-side. Includes subfolder and file counts."
     }
    },
    {
     "name": "Create Folder",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/folders",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "folders"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"name\": \"New Folder\",\n  \"parentFolderId\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a virtual folder within the asset catalog. Does not create anything on the physical storage provider."
     }
    },
    {
     "name": "Delete Folder",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/folders/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "folders",
        "1"
       ]
      },
      "description": "Remove a virtual folder and optionally its contents from the catalog. Physical files on the provider are not affected."
     }
    },
    {
     "name": "List Shortcuts",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/shortcuts/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "shortcuts",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve shared, API-visible storage shortcuts (saved folder views). Used to navigate storage without hardcoding paths."
     }
    },
    {
     "name": "Get Shortcut",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/shortcuts/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "shortcuts",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Fetch detail for a single shortcut by ID. Only accessible if marked as shared and API-visible."
     }
    },
    {
     "name": "Create Shortcut",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/shortcuts",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "shortcuts"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantStorageSourceId\": 100003,\n  \"name\": \"Q2 Sensitive Records\",\n  \"description\": \"HIPAA-flagged customer documents\",\n  \"storagePath\": \"q2-records\",\n  \"isSensitive\": true,\n  \"isShared\": true,\n  \"isApiVisible\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a new storage shortcut. Returns the freshly-created shortcut detail so the caller can reference its storageShortcutId in downstream form layouts or upload calls. Defaults: IsShared=true, IsApiVisible=true unless overridden. Catalog-is-bible: a property flagged IsSensitive=true cannot be routed to a non-sensitive shortcut by a form layout."
     }
    },
    {
     "name": "Update Shortcut",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/shortcuts/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "shortcuts",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantStorageSourceId\": 100003,\n  \"name\": \"Q2 Sensitive Records (renamed)\",\n  \"description\": \"Updated description\",\n  \"isSensitive\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update an existing storage shortcut. System shortcuts (IsSystem=true) cannot be edited via the API and return 409."
     }
    },
    {
     "name": "Delete Shortcut",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/shortcuts/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "shortcuts",
        "1"
       ]
      },
      "description": "Soft-delete a storage shortcut. Returns 409 if any live binary asset or form layout still references it. FileAdmin role lets you delete shortcuts you don't own; FileEdit can delete your own."
     }
    },
    {
     "name": "List Sync Jobs",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sync-jobs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sync-jobs",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve configured sync jobs with their schedule and status. Includes next-run time and last-run details."
     }
    },
    {
     "name": "Get Sync Job",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sync-jobs/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sync-jobs",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Fetch full sync job detail including configuration (source, target, frequency, auto-import rules)."
     }
    },
    {
     "name": "Update Sync Job",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sync-jobs/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sync-jobs",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"isEnabled\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Modify a sync job's schedule, source, target folder, or auto-import behavior without triggering an immediate run."
     }
    },
    {
     "name": "Run Sync Job",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sync-jobs/1/run",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sync-jobs",
        "1",
        "run"
       ]
      },
      "description": "Trigger an immediate manual run of a sync job regardless of schedule. Returns a run ID for tracking."
     }
    },
    {
     "name": "Sync Job Runs",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/sync-jobs/1/runs",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "sync-jobs",
        "1",
        "runs"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "View execution history for a sync job with status, start/end times, and counts of synced/skipped/failed items."
     }
    },
    {
     "name": "Extract ZIP",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets/extract-zip",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets",
        "extract-zip"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assetKey\": \"00000000-0000-0000-0000-000000000000\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Unzip a file into the asset catalog, optionally preserving folder structure. Skips duplicates by default."
     }
    }
   ]
  },
  {
   "name": "Storage Sources (Admin)",
   "description": "Storage provider management — configure cloud storage connections.",
   "item": [
    {
     "name": "List Global Sources",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageSourceList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageSourceList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List all globally configured storage sources. Internal endpoint used by the Portal storage admin UI. Requires AdminSettings role."
     }
    },
    {
     "name": "List Tenant Sources",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List all storage sources available to a tenant (global + tenant overrides). Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Get Tenant Source Detail",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceDetail",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceDetail"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Retrieve full source configuration for a tenant including connection details and access mode. Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Save Tenant Source",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceSave",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceSave"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 0,\n  \"storageProviderTypeId\": 1,\n  \"internalName\": \"NewSource\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create or update a tenant-scoped storage source override (e.g., separate Azure account for one customer). Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Set Default Source",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceSetDefault",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceSetDefault"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Mark a storage source as the tenant's default for non-sensitive asset uploads. Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Set Default Sensitive",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceSetDefaultSensitive",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceSetDefaultSensitive"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Mark a storage source as the default for uploads flagged as sensitive (PII, credentials, etc.). Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Test Connection",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageSourceTestConnectionById",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageSourceTestConnectionById"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Validate storage source credentials by attempting a connection test. Returns success or detailed error. Internal Portal endpoint. Requires AdminSettings role."
     }
    },
    {
     "name": "Delete Tenant Source",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/TenantStorageSourceDelete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "TenantStorageSourceDelete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Remove a tenant-scoped storage source override. Does not affect global sources or existing assets already stored there. Internal Portal endpoint. Requires AdminSettings role."
     }
    }
   ]
  },
  {
   "name": "Storage Explorer",
   "description": "Direct file operations on storage providers — list, upload, download, create/delete folders.",
   "item": [
    {
     "name": "List Files",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageListFiles",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageListFiles"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"path\": \"/\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List files and folders at a path in a configured storage source. Returns a flat list; build tree client-side using folder structure."
     }
    },
    {
     "name": "Upload File",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageUploadFile",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageUploadFile"
       ]
      },
      "description": "Upload a file to a storage source via the Storage Explorer (multipart). The proxy honors the same provider cascade (tenant -> global) the rest of the platform uses + stamps an audit-trail entry on the BinaryAsset. Role: **FileUpload**."
     }
    },
    {
     "name": "Download File",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageDownloadFile",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageDownloadFile"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"path\": \"/test.txt\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Stream a single file from storage with correct MIME type and filename headers."
     }
    },
    {
     "name": "Download as ZIP",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageDownloadZip",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageDownloadZip"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"paths\": [\n    \"/folder1\"\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Download multiple files/folders as a single ZIP archive for bulk export."
     }
    },
    {
     "name": "Create Folder",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageCreateFolder",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageCreateFolder"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"path\": \"/new-folder\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a new folder in storage at the specified path. Provider-level operation, not virtual catalog."
     }
    },
    {
     "name": "Delete Folder",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageDeleteFolder",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageDeleteFolder"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"path\": \"/new-folder\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Delete a folder and optionally its contents from storage. Provider-level operation."
     }
    },
    {
     "name": "Delete File",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageDeleteFile",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageDeleteFile"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"path\": \"/test.txt\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Delete a single file from storage. Provider-level operation."
     }
    }
   ]
  },
  {
   "name": "Binary Assets (Internal)",
   "description": "Internal binary asset management — used by the portal for file tracking.",
   "item": [
    {
     "name": "List Assets",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: list binary assets in the catalog with optional folder/source filtering. Used by Storage Explorer UI. Requires FileView role."
     }
    },
    {
     "name": "Get Detail",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetDetail",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetDetail"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: fetch full asset detail by binary asset ID. Used by Storage Explorer UI. Requires FileView role."
     }
    },
    {
     "name": "Get by Key",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetDetailByKey",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetDetailByKey"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"assetKey\": \"00000000-0000-0000-0000-000000000000\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: fetch asset by GUID key (assetKey). Used by Storage Explorer UI. Requires FileView role."
     }
    },
    {
     "name": "Update Metadata",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetUpdateMeta",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetUpdateMeta"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1,\n  \"displayName\": \"Updated\",\n  \"description\": \"Updated\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: update asset title, description, or folder location. Used by Storage Explorer UI. Requires FileEdit role."
     }
    },
    {
     "name": "Delete Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetDelete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetDelete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: soft-delete asset from catalog with optional physical file deletion. Used by Storage Explorer UI. Requires FileDelete role."
     }
    },
    {
     "name": "Move Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetMove",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetMove"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1,\n  \"targetFolderId\": 2\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: move an asset to a different folder in the catalog. Used by Storage Explorer UI. Requires FileEdit role."
     }
    },
    {
     "name": "Verify Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetVerify",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetVerify"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: validate that a binary asset's metadata matches the physical file on storage. Used by Storage Explorer UI. Requires FileView role."
     }
    },
    {
     "name": "Import Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetImport",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetImport"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"tenantStorageSourceId\": 1,\n  \"filePath\": \"/imported.pdf\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: import a file from storage (external source or another storage account) into the catalog. Used by Storage Explorer UI. Requires FileUpload role."
     }
    },
    {
     "name": "Sync Asset",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/BinaryAssetSync",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "BinaryAssetSync"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"binaryAssetId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: copy or mirror an asset to a different storage source. Used by Storage Explorer UI. Requires FileUpload role."
     }
    },
    {
     "name": "Folder List",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/AssetFolderList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "AssetFolderList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: list virtual folders with hierarchy. Used by Storage Explorer UI folder tree. Requires FileView role."
     }
    },
    {
     "name": "Folder Save",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/AssetFolderSave",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "AssetFolderSave"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"assetFolderId\": 0,\n  \"name\": \"New Folder\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: create or update a virtual folder. Used by Storage Explorer UI. Requires FileEdit role."
     }
    },
    {
     "name": "Folder Delete",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/AssetFolderDelete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "AssetFolderDelete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"assetFolderId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: delete a virtual folder and optionally cascade delete assets. Used by Storage Explorer UI. Requires FileDelete role."
     }
    },
    {
     "name": "Folder Move",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/AssetFolderMove",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "AssetFolderMove"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"assetFolderId\": 1,\n  \"targetFolderId\": 2\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: move a folder and its children to a new parent in the virtual hierarchy. Used by Storage Explorer UI. Requires FileEdit role."
     }
    }
   ]
  },
  {
   "name": "Sync Jobs & Shortcuts",
   "description": "Storage sync job management — shortcuts, scheduling, run history.",
   "item": [
    {
     "name": "List Shortcuts",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageShortcutList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageShortcutList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: list storage shortcuts (saved folder views). Used by Storage Explorer sidebar. Requires FileView role."
     }
    },
    {
     "name": "Save Shortcut",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageShortcutSave",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageShortcutSave"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"storageShortcutId\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: create or update a storage shortcut (saved folder path in a source). Used by Storage Explorer UI. Requires FileEdit role."
     }
    },
    {
     "name": "Delete Shortcut",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/StorageShortcutDelete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "StorageShortcutDelete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"storageShortcutId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: delete a storage shortcut. Used by Storage Explorer UI. Requires FileDelete role."
     }
    },
    {
     "name": "Get Pending Jobs",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobGetPending",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobGetPending"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: retrieve sync jobs that are due to run or currently running. Used by the admin dashboard. Requires AdminSettings role."
     }
    },
    {
     "name": "Toggle Sync Job",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobToggle",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobToggle"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"storageShortcutId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: enable or disable a sync job without deleting it. Used by Storage Explorer sync admin. Requires AdminSettings role."
     }
    },
    {
     "name": "Run History",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobRunList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobRunList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"storageShortcutId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: retrieve execution history and results for a sync job. Used by sync job monitoring UI. Requires AdminSettings role."
     }
    },
    {
     "name": "Run Logs",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobLogList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobLogList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"syncJobRunId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Internal Portal endpoint: fetch detailed logs from a sync job run (item counts, errors, skipped files). Used by sync job run detail view. Requires AdminSettings role."
     }
    },
    {
     "name": "Run All (Engine)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobRunAll",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobRunAll"
       ]
      },
      "description": "Trigger the SyncEngine to run every enabled sync job for every tenant NOW (out-of-band the scheduler). Returns per-tenant counts. Heavy operation; intended for the ServiceProofSync console + admin maintenance, not regular use. Role: **AdminUser** (global tenant)."
     }
    },
    {
     "name": "Run Tenant (Engine)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/SyncJobRunTenant",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "SyncJobRunTenant"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Trigger the SyncEngine to run every enabled sync job for ONE tenant NOW. The job runs in-process; the response carries the per-job outcome (rows affected + ms). Role: **AdminUser**."
     }
    }
   ]
  },
  {
   "name": "API Keys",
   "description": "API key lifecycle — create, list, revoke. Keys are bearer tokens with scoped roles.",
   "item": [
    {
     "name": "List API Keys",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/ApiKeyList",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "ApiKeyList"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List all API keys issued by or to the caller. Shows key name, creation date, expiration, and last-used timestamp. Requires AdminIntegration role."
     }
    },
    {
     "name": "Create API Key",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/ApiKeyCreate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "ApiKeyCreate"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"name\": \"Integration Key\",\n  \"roleFlag\": 255\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Mint a tenant-scoped API key with the given role bitmask + optional ExpiresDate. The raw key is returned ONCE in the response - store it immediately; the platform only keeps the hash. Future rotations require minting a new key + deleting the old. Role: **AdminIntegration**."
     }
    },
    {
     "name": "Revoke API Key",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/ApiKeyRevoke",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "ApiKeyRevoke"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"userId\": 2,\n  \"tenantUserId\": 4,\n  \"apiKeyId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Immediately revoke an API key. The next request with that token gets 401. Takes effect instantly with no grace period."
     }
    }
   ]
  },
  {
   "name": "Workflow API",
   "item": [
    {
     "name": "List Workflows",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Browse the tenant Workflows (Pillar 11). Each row carries the workflow InternalName + display Name (in the requested language) + ContextDefinition binding + step counts. Workflows are authored here and delivered to the field via Messaging / Surfaces / Mobile. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Create Workflow",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"EquipmentInspection\",\n  \"contextDefinitionId\": 2,\n  \"storageShortcutId\": null,\n  \"defaultAnchorJson\": null,\n  \"outputParamsJson\": null,\n  \"displays\": [\n    {\n      \"languageRegionId\": 69,\n      \"name\": \"Equipment Inspection\",\n      \"description\": \"Field inspection workflow\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a workflow. Optional SETTINGS (proc-backed, settable here): `contextDefinitionId` — bind the anchor ContextDefinition so steps resolve [[ ]] tokens (the keystone of a contextual workflow); `storageShortcutId` — default media destination; `defaultAnchorJson` — a JSON string like {\"WorkOrderNumber\":\"WO-5001\"} auto-applied on Test / one-click Run; `outputParamsJson` — params resolved at WorkflowCompleted. Workflows start empty; add steps next, then `/publish`. BuilderAdmin scope."
     }
    },
    {
     "name": "Get Workflow Detail",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Fetch one Workflow full canvas: every Step, StepOption, StepAction (incl. E3 RunActionPack bindings via `actionPackInstanceId`), Display rows per language, and the bound ContextDefinition. The runner published snapshot is built from this shape. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Update Workflow",
     "request": {
      "method": "PUT",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"UpdatedName\",\n  \"contextDefinitionId\": 2,\n  \"storageShortcutId\": null,\n  \"defaultAnchorJson\": null,\n  \"outputParamsJson\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change a workflow's name or SETTINGS (contextDefinitionId, storageShortcutId, defaultAnchorJson, outputParamsJson). An omitted setting is PRESERVED — a name-only update never clears the context binding. BuilderAdmin scope required."
     }
    },
    {
     "name": "Delete Workflow",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}"
       ]
      },
      "description": "Delete a workflow. Fails with 409 if any steps exist — delete all steps first. BuilderAdmin scope required."
     }
    },
    {
     "name": "Save Workflow Display",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/displays",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "displays"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 69,\n  \"name\": \"Equipment Inspection\",\n  \"description\": \"Updated description\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Set the name and description for a workflow in a specific language. Supports multi-language workflows. BuilderAdmin scope required."
     }
    },
    {
     "name": "Publish Workflow",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/publish",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "publish"
       ]
      },
      "description": "Publish the workflow — freeze a versioned, runnable snapshot the field runner + messaging delivery execute. Runs the context-entity sync first (the same C# step the Portal Publish button does), so the snapshot is fully hydrated. BuilderAdmin scope."
     }
    },
    {
     "name": "Test-Publish Workflow (stage slot)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/test",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "test"
       ]
      },
      "description": "Publish to the TEST/stage slot — same context-entity sync into a separate snapshot the Stage test runner executes, without touching the live published version. BuilderAdmin scope."
     }
    },
    {
     "name": "Add Step",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"EquipmentMake\",\n  \"stepTypeId\": 2,\n  \"position\": 1,\n  \"propertyId\": null,\n  \"entityId\": null,\n  \"isRequired\": true,\n  \"defaultValue\": \"[[Customer.OrgName]]\",\n  \"isSensitive\": false,\n  \"imageRequirementId\": 2,\n  \"noteRequirementId\": null,\n  \"binaryInputSourceId\": 3,\n  \"renderAsJson\": \"{\\\"mode\\\":\\\"dropdown\\\",\\\"source\\\":\\\"bundle\\\",\\\"bundleSlot\\\":\\\"Equipment\\\",\\\"keyField\\\":\\\"Make\\\",\\\"displayField\\\":\\\"[[Make]] - [[Model]]\\\"}\",\n  \"preferSurfaceJson\": \"[\\\"Mobile\\\",\\\"Rcs\\\",\\\"Web\\\"]\",\n  \"sourceKind\": 1,\n  \"sourceArtifactId\": 100042,\n  \"sourceField\": \"Make\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Add a step. stepTypeId: 1 Info, 2 Data (bind propertyId — the control comes from the property's DataType; e.g. Attachment=7 / Binary=10 photo, Signature=8), 3 Picker, 4 Split, 5 MultiSplit, 6 ContextSplit, 7 ValueSplit, 8 Loop, 9 EntityData (bind entityId), 11 Choice, 12 Files. Capture knobs: isSensitive; imageRequirementId / noteRequirementId (ride-along photo/note: null/2/3 = none/optional/required); binaryInputSourceId (1 Camera / 2 Gallery / 3 Both). Files step (12) bounds: filesFreeForm (true = free-form collection vs fixed slots) + filesMinCount + filesMaxCount. Provenance for a field sourced from a View/Report/Form: sourceKind (1/2/3), sourceArtifactId, sourceField. ValueSplit/sourced: sourceStepId + sourcePropertyId. BuilderAdmin scope."
     }
    },
    {
     "name": "Get Step Detail",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "One Step + its options + its displays + its property/entity binding. Includes the step ImageAssetKey (per-option real images for messaging carousels), ImageRequirementId / NoteRequirementId (ride-along gates), and BinaryInputSourceId (camera/library capture mode). Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Update Step",
     "request": {
      "method": "PUT",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"UpdatedStepName\",\n  \"isRequired\": false,\n  \"isSensitive\": true,\n  \"imageRequirementId\": 3,\n  \"noteRequirementId\": 2,\n  \"binaryInputSourceId\": 1,\n  \"renderAsJson\": \"{\\\"mode\\\":\\\"memo\\\"}\",\n  \"preferSurfaceJson\": \"[\\\"Mobile\\\",\\\"Web\\\"]\",\n  \"filesFreeForm\": false,\n  \"filesMinCount\": null,\n  \"filesMaxCount\": null,\n  \"sourceStepId\": null,\n  \"sourcePropertyId\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Modify a step. Partial update — only send what changes; the rest is PRESERVED (incl. provenance + Files config, which a bare update no longer nulls). Fields: isRequired, defaultValue, isSensitive, displayName/displayDescription, position, renderAsJson, preferSurfaceJson, imageRequirementId / noteRequirementId (ride-along), binaryInputSourceId (1 Camera / 2 Gallery / 3 Both), storageShortcutId, filesFreeForm / filesMinCount / filesMaxCount (Files step), sourceKind/sourceArtifactId/sourceField + sourceStepId/sourcePropertyId (provenance/value-split source). BuilderAdmin scope."
     }
    },
    {
     "name": "Delete Step",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}"
       ]
      },
      "description": "Delete a step and cascade-delete its displays, options, and child steps in split paths. Closes position gaps. BuilderAdmin scope required."
     }
    },
    {
     "name": "Duplicate Step",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/duplicate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "duplicate"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Clone a Step within its workflow (with every option, display, and binding). Used for fast canvas authoring. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Move Step Up",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/move",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "move"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": -1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Swap a step's position with its previous sibling. Respects split context (only swaps within the same branch). BuilderAdmin scope required."
     }
    },
    {
     "name": "Move Step Down",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/move",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "move"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Swap a step's position with its next sibling. Respects split context (only swaps within the same branch). BuilderAdmin scope required."
     }
    },
    {
     "name": "Reorder Steps",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/reorder",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "reorder"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Replace the Position column for a list of steps in one transactional call - the canonical re-order pattern after a drag-and-drop in the builder. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Save Step Action (SetContext)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/actions",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "actions"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionTypeId\": 6,\n  \"contextKey\": \"Job.JobStatus\",\n  \"valueExpression\": \"[[Answers.Severity]]\",\n  \"conditionJson\": \"{\\\"logic\\\":\\\"AND\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Job.Priority\\\",\\\"op\\\":\\\"equals\\\",\\\"value\\\":\\\"High\\\"},{\\\"field\\\":\\\"ServiceLocation.State\\\",\\\"op\\\":\\\"equals\\\",\\\"value\\\":\\\"CT\\\"}]}\",\n  \"position\": 1,\n  \"setScope\": 3,\n  \"targetEntityId\": 100015,\n  \"targetPropertyId\": 100042\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create or update a SetContext action on a step. Omit stepActionId to create. Read actions back via the step-detail POST (its `actions` array). BuilderAdmin scope. Set `setScope` 2/3 + `targetEntityId`/`targetPropertyId` to PERSIST the resolved value onto the EntityInstance (status flips, `[[completedAt]]` timestamps); omit for Bundle-only (in-run)."
     }
    },
    {
     "name": "Delete Step Action",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/actions/{{stepActionId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "actions",
        "{{stepActionId}}"
       ]
      },
      "description": "Soft-delete one StepAction by id. The workflow + step + every other action survive. After delete, re-publish the workflow so the next run snapshot drops the action. Role: **BuilderAdmin**."
     }
    },
    {
     "name": "Save Step Option (branch)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/options",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "options"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"PrioHighCt\",\n  \"position\": 1,\n  \"optionBehaviorTypeId\": 1,\n  \"isRequired\": false,\n  \"value\": \"[[Job.Priority == 'High' && ServiceLocation.State == 'CT']]\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create or update a step option (split/choice branch). Omit stepOptionId to create. value can be a literal, * (default), an inline array, or a [[bool]] expression for ContextSplit/ValueSplit. Read options back via the step-detail POST. BuilderAdmin scope."
     }
    },
    {
     "name": "Delete Step Option",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/options/{{stepOptionId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "options",
        "{{stepOptionId}}"
       ]
      },
      "description": "Delete a step option by id. 409 Conflict if child steps reference its branch. BuilderAdmin scope."
     }
    },
    {
     "name": "Save Step Display (per-language)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/display",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "display"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 69,\n  \"name\": \"Job brief for [[upper(Customer.OrgName)]]\",\n  \"description\": \"WO [[Job.WorkOrderNumber]] - [[Job.ServiceType]] ([[Job.Priority]] priority).\",\n  \"imageLabel\": \"Photo of the dataplate\",\n  \"noteLabel\": \"Note any damage\",\n  \"isTranslationLocked\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a per-language display (Name/Description/ImageLabel/NoteLabel) for a step. languageRegionId + name required. BuilderAdmin scope."
     }
    },
    {
     "name": "Save Step Option Display (per-language)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/workflows/{{workflowId}}/steps/{{stepId}}/options/{{stepOptionId}}/display",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "workflows",
        "{{workflowId}}",
        "steps",
        "{{stepId}}",
        "options",
        "{{stepOptionId}}",
        "display"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageRegionId\": 69,\n  \"name\": \"High + CT\",\n  \"description\": \"High priority AND a Connecticut site.\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a per-language display (label/description) for a step option. languageRegionId + name required. BuilderAdmin scope."
     }
    }
   ]
  },
  {
   "name": "Jobs - Conductor4",
   "description": "The jobs engine (the J-track of the grand unification): Job Set -> Kit -> Job -> Workflow -> Step. Sets gate the river of work, kits are saved FilterGroups + workflows + a run strategy, a job is a kit instantiated against an anchor, assigned and tracked to done. Reference: docs/public/Integrators/Jobs-API.md. Roles: sets/kits = BuilderAdmin; dispatch/qualify/cancel = AdminIntegration; worklist/launch = DataView.",
   "item": [
    {
     "name": "List job sets",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "list"
       ]
      },
      "description": "All job sets for the tenant with kit counts. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Create / update a job set",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets"
       ]
      },
      "description": "Upsert by `jobSetId` (omit/0 = create). `flowRulesJson` is a canonical FilterGroup over the flattened bundle (dotted paths, e.g. `Job.ServiceType`). Optional `name`/`description` write the display row. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Acme_SpringMaintenance\",\n  \"flowRulesJson\": \"{\\\"logic\\\":\\\"and\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Job.ServiceType\\\",\\\"op\\\":\\\"in\\\",\\\"value\\\":[\\\"Maintenance\\\",\\\"Inspection\\\"]}]}\",\n  \"isShared\": true,\n  \"name\": \"Spring Maintenance\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Get a job set (by id or name)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/Acme_SpringMaintenance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "Acme_SpringMaintenance"
       ]
      },
      "description": "Detail by numeric id OR friendly InternalName. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Delete a job set",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/Acme_SpringMaintenance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "Acme_SpringMaintenance"
       ]
      },
      "description": "Soft-delete a JobSet by id or InternalName. JobSets are the routing table that maps flowing context items to one or more Kits via per-link FilterGroups (first match wins). Soft-delete preserves audit + lets a downstream agent re-link kits later. The JobSet -> Kit links are NOT auto-deleted - unlink them first or the next router pass will still see the orphaned rules. Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "List a set's kit links",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/Acme_EmergencyResponse/kits",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "Acme_EmergencyResponse",
        "kits"
       ]
      },
      "description": "The set's routing table in rule order (first match wins). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Link a kit to a set",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/Acme_EmergencyResponse/kits/link",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "Acme_EmergencyResponse",
        "kits",
        "link"
       ]
      },
      "description": "Adds/updates a routing link. `routeFilterJson` (FilterGroup) decides which flowing items this kit claims; NULL = fallback claim. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"workflowKitId\": 3,\n  \"routeFilterJson\": \"{\\\"logic\\\":\\\"and\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Job.WorkDescription\\\",\\\"op\\\":\\\"contains\\\",\\\"value\\\":\\\"leak\\\"}]}\",\n  \"sortOrder\": 10\n}"
      }
     },
     "response": []
    },
    {
     "name": "Unlink a kit from a set",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/sets/kits/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "sets",
        "kits",
        "1"
       ]
      },
      "description": "Soft-delete one JobSet -> Kit link by JobSetKitId. Removes the link FilterGroup + the router precedence row; the underlying Kit + Set survive. A subsequent flow that would have routed through this link now falls through to the next matching link (or no link). Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "List workflow kits",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "list"
       ]
      },
      "description": "All kits with item counts. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Create / update a kit",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits"
       ]
      },
      "description": "Upsert by `workflowKitId`. `anchorContextDefinitionId` binds the job's world (hydrated at dispatch); `jobFilterJson` = which records ARE jobs for this kit; `techQualifyFilterJson` = who qualifies (`Tech.TenantUserId` available); `runStrategyId` 1 RcsPreferred / 2 MobileOfflineKickoff / 3 WebPreferred. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Acme_SpringPMKit\",\n  \"anchorContextDefinitionId\": 2,\n  \"jobFilterJson\": \"{\\\"logic\\\":\\\"and\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Job.ServiceType\\\",\\\"op\\\":\\\"in\\\",\\\"value\\\":[\\\"Maintenance\\\",\\\"Inspection\\\"]}]}\",\n  \"runStrategyId\": 3,\n  \"defaultReminderAfterMinutes\": null,\n  \"isShared\": true,\n  \"name\": \"Spring PM Kit\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Get a kit + its items",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/Acme_SpringPMKit",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "Acme_SpringPMKit"
       ]
      },
      "description": "Kit detail PLUS its workflow items in assembly order (incl. each item's includeFilterJson - the magic-assembly knob). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Delete a kit",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/Acme_SpringPMKit",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "Acme_SpringPMKit"
       ]
      },
      "description": "Blocked (409) while live jobs (status 2-5) reference the kit - cancel them first. Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "List a kit's items",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/Acme_SpringPMKit/items",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "Acme_SpringPMKit",
        "items"
       ]
      },
      "description": "The kit's workflows in assembly order. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Add / update a kit item",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/Acme_SpringPMKit/items/save",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "Acme_SpringPMKit",
        "items",
        "save"
       ]
      },
      "description": "`includeFilterJson` NULL = static (always assembles); set = dynamic include - only jobs whose hydrated context matches get this workflow (the premium-customer Site Audit demo). `reminderAfterMinutes` seeds the first nudge at dispatch. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"workflowId\": 100005,\n  \"sortOrder\": 20,\n  \"isRequired\": true,\n  \"includeFilterJson\": \"{\\\"logic\\\":\\\"and\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Customer.OrgName\\\",\\\"op\\\":\\\"equals\\\",\\\"value\\\":\\\"Eastern Plaza Holdings\\\"}]}\",\n  \"reminderAfterMinutes\": 30\n}"
      }
     },
     "response": []
    },
    {
     "name": "Remove a kit item",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/kits/items/2",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "kits",
        "items",
        "2"
       ]
      },
      "description": "Soft-delete one Kit -> Workflow item by WorkflowKitItemId. The workflow definition + the Kit + every other Kit item survive; only this Kit assembly drops the workflow from its stage list. Sequenced Kits re-evaluate gates around the gap (a deleted Travel step does NOT keep its downstream stages Waiting). Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "Filter fields (the authoring oracle)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/filter-fields",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "filter-fields"
       ]
      },
      "description": "DISCOVER before you author. Returns the typed filterable fields (dotted path + DataType + the operators VALID for that type + Choice/Boolean option values) for a kit/jobset/context, plus the FilterGroup wire shape + a worked example + the save targets + the apply-time note (filters run at DISPATCH against the hydrated context). Pass `contextName`, OR a kit (`kitInternalName` / `workflowKitId`) whose bound context is resolved for you. Same schema the human builder's dropdown uses. Copilot/MCP tool: `describe_job_filter_fields`. Scoped to Conductor4 kit/jobset filters (NOT Data Studio filters). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"kitInternalName\": \"Acme_EmergencyPlumbingKit\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Validate a filter (self-correct)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/filter/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "filter",
        "validate"
       ]
      },
      "description": "VALIDATE a drafted FilterGroup against a context's field schema BEFORE saving. Returns `{ valid, issues:[{path,severity,message}] }`: errors block (unknown field, operator not valid for the field's type, missing value, `between` without value2), warnings advise (off-list Choice value). Pass `filterJson` + `contextName` (or a kit). Copilot/MCP tool: `validate_job_filter`. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"kitInternalName\": \"Acme_EmergencyPlumbingKit\",\n  \"filterJson\": \"{\\\"logic\\\":\\\"and\\\",\\\"rules\\\":[{\\\"field\\\":\\\"Job.WorkDescription\\\",\\\"op\\\":\\\"contains\\\",\\\"value\\\":\\\"leak\\\"}]}\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Qualify (dry-run preview)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/qualify",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "qualify"
       ]
      },
      "description": "Same body as dispatch, ZERO writes (`jobInstanceId` stays 0) - the dispatcher's preview: hydrate + gates + assembly. 409 when the job/tech does not qualify; 422 when a keyed anchor is missing its anchorIdentity. anchorIdentity keys are PROPERTY INTERNAL NAMES sent VERBATIM (do not camelCase them). Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"kitInternalName\": \"Acme_SpringPMKit\",\n  \"anchorIdentity\": {\n    \"WorkOrderNumber\": \"WO-5002\"\n  },\n  \"assignedTenantUserId\": 4\n}"
      }
     },
     "response": []
    },
    {
     "name": "Qualify the SEQUENCED kit (flow gates preview)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/qualify",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "qualify"
       ]
      },
      "description": "The J-C flow demo: the sequenced kit previews its stages with their DISPATCH statuses - Travel (Immediate) = 1 NotStarted; Site Audit + Full PM (AfterPrevious) and Closeout (OnRule on Workflows.Acme_FullPMSpring.Status) = 5 Waiting. Dispatch it and the advance pass promotes each stage as its gate opens (a Waiting stage's /start returns 409 until then); the job auto-completes when every required stage is done. See Jobs-API.md section 5.5. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"kitInternalName\": \"Acme_SequencedPMKit\",\n  \"anchorIdentity\": {\n    \"WorkOrderNumber\": \"WO-5002\"\n  },\n  \"assignedTenantUserId\": 4\n}"
      }
     }
    },
    {
     "name": "Dispatch a job",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/dispatch",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "dispatch"
       ]
      },
      "description": "Kit + anchor + tech -> a tracked JobInstance: hydrate the kit's anchor context -> jobFilter + techQualify gates -> ASSEMBLE (static items + matching dynamic items) -> JobInstance (Dispatched, the hydrate envelope on metaJson) + one JobWorkflowInstance per assembled item + Pending reminders per the item/kit cadence. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"kitInternalName\": \"Acme_SpringPMKit\",\n  \"anchorIdentity\": {\n    \"WorkOrderNumber\": \"WO-5002\"\n  },\n  \"assignedTenantUserId\": 4,\n  \"deliveryChannel\": \"Web\",\n  \"anchorLabel\": \"WO-5002 - Eastern Plaza (Boston PM)\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Worklist (filter by tech / status / kit)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "list"
       ]
      },
      "description": "The worklist: every job with kit name, anchor label, tech, lifecycle timestamps, and progress counts (workflowCount / completedWorkflowCount). Filters optional - omit or 0 = all. JobStatus: 1 Draft / 2 Scheduled / 3 Offered / 4 Dispatched / 5 InProgress / 6 Completed / 7 Cancelled / 8 Expired. Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assignedTenantUserId\": 4,\n  \"jobStatusId\": 0\n}"
      }
     },
     "response": []
    },
    {
     "name": "My nudges (in-app poll - the all-mobile seam)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/nudges",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "nudges"
       ]
      },
      "description": "App-only clients receive no texts (a WASM PWA has no push) - the app POLLS this and pops the nudges on its panels. Returns the caller's (or an impersonated user's via assignedTenantUserId) Pending reminders across their live jobs, recipient resolved per-stage (a driver only sees the Deliver-stage nudges that are theirs), ordered by due time. messageText carries the ready-nudge wording when set. Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assignedTenantUserId\": 0\n}"
      }
     }
    },
    {
     "name": "Job detail",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5"
       ]
      },
      "description": "One job + progress counts + metaJson (the dispatch-time hydrate envelope - the job's world). Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Job workflows (the jump menu)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5/workflows",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5",
        "workflows"
       ]
      },
      "description": "The job's workflows in assembly order, each with TWO statuses: jobWorkflowStatusId (Conductor tracking: 1 NotStarted / 2 InProgress / 3 Completed / 4 Skipped) and liveRunnerStatusId (the REAL runner state from WorkflowInstanceTrack - NULL until launched). Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Addable workflows for a job (ad-hoc picker source)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5/workflows/addable",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5",
        "workflows",
        "addable"
       ]
      },
      "description": "Every published workflow MINUS those already live on the job, as [{ workflowId, internalName, name }] - the source for the 'add a workflow' picker. Pair with /workflows/add. Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Add an ad-hoc workflow to a job",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5/workflows/add",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5",
        "workflows",
        "add"
       ]
      },
      "description": "Tag ANY published workflow onto this job as a new ad-hoc stage (WorkflowKitItemId null). Body { workflowId } -> { jobWorkflowInstanceId, alreadyOnJob }; then /start it like any stage. De-dupes a workflow already live on the job. Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"workflowId\": 12\n}"
      }
     },
     "response": []
    },
    {
     "name": "Start a job workflow (lazy launch)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5/workflows/11/start",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5",
        "workflows",
        "11",
        "start"
       ]
      },
      "description": "Creates the REAL runner WorkflowInstance through the WorkflowStart semantics: published snapshot -> pinned-context hydrate against the JOB's anchorIdentity -> instance + bundle on MetaJson. Idempotent: re-start returns the same instance with alreadyStarted=true. First start promotes the job Dispatched -> InProgress. Role: **DataView**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Assign a job workflow (per-stage assignee)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/{{jobInstanceId}}/workflows/{{jobWorkflowInstanceId}}/assign",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "{{jobInstanceId}}",
        "workflows",
        "{{jobWorkflowInstanceId}}",
        "assign"
       ]
      },
      "description": "J-C per-stage assignment: set (or clear with 0) THIS stage's assignee - the claim on a pool stage (kitchen vs driver vs tech). The worklist matches stage assignees too, so a driver's /jobs/list shows the order whose Deliver stage is theirs. Reminder nudges resolve to the stage assignee first. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assignedTenantUserId\": 7\n}"
      }
     }
    },
    {
     "name": "Cancel a job",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/jobs/5/cancel",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "jobs",
        "5",
        "cancel"
       ]
      },
      "description": "Job -> Cancelled, unfinished workflows -> Skipped, pending reminders -> Cancelled. Idempotent; a Completed job returns 409. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Operate the reminder sweep (enable / disable)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs/22/enabled",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs",
        "22",
        "enabled"
       ]
      },
      "description": "The JobsSweep Timer pack is operated through the Actions API: this flips the install's kill switch (enable re-arms NextRunUtc automatically - the timer invariant). Settings (maxRows / dryRun / messageTemplate) ride the install's settingsJson via POST /api/v1/actions/installs. The sweep ticks every FrequencyMinutes: due Pending reminders -> decide (progress cancels) -> nudge the tech's handset -> re-queue per rule cadence -> escalate at the cap. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"isEnabled\": true\n}"
      }
     },
     "response": []
    }
   ]
  },
  {
   "name": "Entity Data",
   "description": "Entity instance CRUD, history, and saved views. Requires Data roles (DataImport, DataView, DataEdit, DataDelete, or DataAdmin).",
   "item": [
    {
     "name": "Save Instance",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"key1\": \"CUST-DEMO-{{$timestamp}}\",\n  \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"CUST-DEMO\\\", \\\"OrgName\\\": \\\"Postman Demo Customer\\\"}\",\n  \"importMode\": \"upsert\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a single Customer instance. key1 is timestamped so reruns don't 409 (each run creates a new row). DataJson uses friendly InternalName keys (CustomerNumber, OrgName) — translator converts to canonical numeric keys internally."
     }
    },
    {
     "name": "Save Instance — child via parentKey1 (resolve parent by composite key)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"key1\": \"CUST-001\",\n  \"key2\": \"LOC-PKR-{{$timestamp}}\",\n  \"parentKey1\": \"CUST-001\",\n  \"dataJson\": \"{\\\"CustomerNumber\\\":\\\"CUST-001\\\",\\\"LocationNumber\\\":\\\"LOC-PKR\\\",\\\"City\\\":\\\"Hartford\\\",\\\"State\\\":\\\"CT\\\"}\",\n  \"importMode\": \"upsert\"\n}"
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/ServiceLocation/instances",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "ServiceLocation",
        "instances"
       ]
      },
      "description": "Single-record save where the server resolves the parent EIRecordId by matching parentKey1/2/3 against the parent entity's composite key + the schema-graph composition relationship. No need to pre-look-up the Customer's numeric id. PR #249 closed the previous 'bulk-only parent resolution' gap; this request demonstrates parity. Response carries eiRecordId; re-fetch and check parentEIRecordId for verification."
     },
     "response": []
    },
    {
     "name": "Bulk Save Instances",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/bulk",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "bulk"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"importMode\": \"upsert\",\n  \"items\": [\n    {\n      \"key1\": \"BULK-DEMO-{{$timestamp}}-1\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"BULK-A\\\", \\\"OrgName\\\": \\\"Bulk Demo A\\\"}\"\n    },\n    {\n      \"key1\": \"BULK-DEMO-{{$timestamp}}-2\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"BULK-B\\\", \\\"OrgName\\\": \\\"Bulk Demo B\\\"}\"\n    },\n    {\n      \"key1\": \"BULK-DEMO-{{$timestamp}}-3\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"BULK-C\\\", \\\"OrgName\\\": \\\"Bulk Demo C\\\"}\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Bulk save 3 Customer instances with friendly DataJson keys. The property name-to-id map is built ONCE for the whole batch — 10k-row imports add one DB call, not 10k. Inserted/Updated/Skipped counts returned."
     }
    },
    {
     "name": "Bulk Save Instances — Equipment under CUST-001/LOC-001 (parent resolved by composite key)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"importMode\": \"upsert\",\n  \"items\": [\n    {\n      \"key1\": \"EQUIP-PKR-{{$timestamp}}\",\n      \"key2\": \"CUST-001\",\n      \"key3\": \"LOC-001\",\n      \"parentKey1\": \"CUST-001\",\n      \"parentKey2\": \"LOC-001\",\n      \"dataJson\": \"{\\\"EquipmentNumber\\\":\\\"EQUIP-PKR\\\",\\\"CustomerNumber\\\":\\\"CUST-001\\\",\\\"LocationNumber\\\":\\\"LOC-001\\\",\\\"EquipmentMake\\\":\\\"Carrier\\\",\\\"EquipmentModel\\\":\\\"30RB-040\\\"}\"\n    }\n  ]\n}"
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/bulk",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "bulk"
       ]
      },
      "description": "Bulk save where each row carries parentKey1/2/3 instead of a numeric ParentEIRecordId. The proc walks the parent entity's index by (parentKey1, parentKey2, parentKey3) and applies the schema-graph relationship check. Equipment row's parentKey1+2 = (CUST-001, LOC-001) resolves to the matching ServiceLocation. Same shape works on the single-record /instances endpoint after PR #249."
     },
     "response": []
    },
    {
     "name": "List Instances",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 10\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Paged list of Customer instances. Friendly entity name in the URL. Swap 'Customer' for any entity InternalName or numeric EntityId."
     }
    },
    {
     "name": "Get Instance",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "CUST-001"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Get a Customer instance by externalKey CUST-001 (seeded — Eastern Plaza Holdings). Phase P2.3.1 — {idOrKey} accepts either a numeric EIRecordId or a friendly externalKey."
     }
    },
    {
     "name": "Get Instance — RESOLVED BINARY (data sibling, FQDN url)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/EQ-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "EQ-001"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Read an instance that has a binary field (DataType Attachment/Signature/Binary). Every read returns BOTH `dataJson` (raw canonical) AND a friendly `data` sibling where each binary field is auto-resolved into a ready-to-use object — no base-joining, no extension guessing:\n\n  \"data\": {\n    \"Dataplate\": {\n      \"key\": \"8c1a8b3e-...\",\n      \"name\": \"Left Front Corner\",\n      \"url\": \"https://wtfapi.serviceproof.net/api/binary/42/8c1a8b3e-...\",\n      \"directUrl\": \"https://acme.blob.core.windows.net/acme-corp/equipment/plate.jpg\",\n      \"mimeType\": \"image/jpeg\",\n      \"sensitive\": false,\n      \"sourceName\": \"AcmePrimary\",\n      \"fileSize\": 127543\n    }\n  }\n\nRULES for any consumer (humans AND AI agents):\n- `url` is ABSOLUTE (FQDN) — drop it straight into <img src> / <a href> / <video src>, even cross-origin. No prefixing.\n- SENSITIVE asset: `url` arrives PRE-SIGNED (?sig=&exp=&u=, 15-min TTL) so it renders on a plain <img> with no Authorization header; `directUrl` is withheld by design.\n- `directUrl` is present only for non-sensitive assets on a public (CDN) source — use it to skip the proxy hop.\n- Pick the element by `mimeType` (image/* | application/pdf | video/* | audio/*) — NEVER by the URL (the key-proxy URL is extensionless on purpose).\n- Download the bytes: GET the `url`, or GET /api/v1/assets/{key}/download (Bearer OR signed query).\n- Full metadata for one asset: POST /api/v1/assets/{key} (Asset API > Get Asset)."
     }
    },
    {
     "name": "Delete Instance",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/CUST-DEMO-nonexistent",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "CUST-DEMO-nonexistent"
       ]
      },
      "description": "DELETE a Customer instance by externalKey. Expects 404 as shown (nonexistent key) so you don't accidentally nuke seeded data. To actually delete, run 'Save Instance' first with a throwaway key, then replace this URL's key segment with yours. ⚠ For composite-keyed entities (ServiceLocation, Equipment) Kestrel rejects URL-encoded slashes — use 'Delete Instance — by composite key (POST body)' below instead.",
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Delete Instance — by composite key (POST body)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/ServiceLocation/instances/delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "ServiceLocation",
        "instances",
        "delete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"key1\": \"CUST-001\",\n  \"key2\": \"LOC-DEMO-nonexistent\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Body-driven soft-delete by composite key (Key1/Key2/Key3). Integrator-friendly — no URL escaping required for slashes. The endpoint accepts THREE shapes (priority order):\n\n1. { \"eiRecordId\": 145 } — direct ID lookup\n2. { \"externalKey\": \"CUST-001 / LOC-001\" } — server splits on \" / \"\n3. { \"key1\": \"...\", \"key2\": \"...\", \"key3\": \"...\" } — explicit parts (cleanest)\n\nReturns 200 { deleted: true } on success, 404 if no live row matches, 409 if child instances reference it. The example sends a nonexistent LOC- key to demonstrate the 404 path safely. To actually delete, run 'Save Instance' on a composite-keyed entity first with throwaway keys, then put those keys here."
     }
    },
    {
     "name": "Delete Instance — by externalKey (POST body)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "delete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"externalKey\": \"EQ-DEMO-nonexistent / CUST-001 / LOC-001\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same body-driven delete endpoint as above, demonstrating the externalKey path. The server splits on ' / ' (space-slash-space, the platform's canonical composite-key display separator) and feeds key1/key2/key3 to the soft-delete proc. Useful when integrators or AI agents already have the externalKey string from a list / detail / report response and don't need to parse it apart."
     }
    },
    {
     "name": "Bulk Delete Instances — composite-key TVP",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/bulk-delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "bulk-delete"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"items\": [\n    { \"key1\": \"CUST-DEMO-nonexistent-1\" },\n    { \"key1\": \"CUST-DEMO-nonexistent-2\" },\n    { \"key1\": \"CUST-DEMO-nonexistent-3\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Bulk soft-delete by composite-key TVP. SINGLE round trip for N rows — replaces N×DELETE-by-id calls in wipe / overwrite-import scenarios.\n\nResponse shape: { deletedCount, requestedCount, notFoundCount }\n\nThroughput: ServiceProofSync's wipe of a 972-row Acme tenant is ~6 HTTP calls (one per entity) using this endpoint, vs. ~972 with the per-row pattern. At enterprise scale (100K+ rows) the savings dominate.\n\nAtomic: the whole batch commits or rolls back. If any row references a child instance (FK 547), the entire delete returns 409 — caller wipes child entities first (reverse-dependency order; see Sync-Toolkit doctrine).\n\nThe example sends nonexistent keys to demonstrate the notFoundCount=3 path safely without nuking seeded data."
     }
    },
    {
     "name": "Instance History",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/12/history",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "12",
        "history"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Time-series history for Equipment EIRecordId=12. Returns every hist.EntityInstance revision plus the current live row. {idOrKey} accepts numeric or externalKey — replace 12 with an externalKey like 'RDG-00001' for a reading."
     }
    },
    {
     "name": "List Views",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/views/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "views",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "List saved views on the Customer entity. Views are per-entity filtered pivots defined in Data Studio. Optionally filter with ?isApiVisible=true for API-visible views only."
     }
    },
    {
     "name": "Save View",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/views",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "views"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"PostmanDemoView-{{$timestamp}}\",\n  \"name\": \"Postman Demo View\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"viewJson\": \"{\\\"columns\\\": [{\\\"propertyInternalName\\\": \\\"CustomerNumber\\\"}, {\\\"propertyInternalName\\\": \\\"OrgName\\\"}], \\\"filters\\\": {\\\"propertyFilters\\\": {\\\"logic\\\": \\\"AND\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Customer\\\", \\\"property\\\": \\\"CustomerNumber\\\", \\\"op\\\": \\\"startsWith\\\", \\\"value\\\": \\\"CUST\\\"}], \\\"groups\\\": null}}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a view on Customer with 2 columns + a startsWith filter. internalName is timestamped so reruns always create a new view. Flip isApiVisible to false to keep the view Portal-only."
     }
    },
    {
     "name": "Delete View",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/views/99999",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "views",
        "99999"
       ]
      },
      "description": "DELETE a view by id. 99999 is intentional — expects 404 safely. To really delete, run 'Save View' first, grab the returned entityInstanceViewId, then call DELETE with that id.",
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Get Instance (friendly — by externalKey in URL)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "CUST-001"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P2.3.1 — {idOrKey} accepts the friendly externalKey 'CUST-001' directly. Same endpoint accepts a numeric EIRecordId; integrators can use whichever they have. Cross-entity keys return 404 so stale numerics don't leak wrong data."
     },
     "response": []
    },
    {
     "name": "Save Instance (friendly DataJson keys)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"key1\": \"CUST-COOK-{{$timestamp}}\",\n  \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"CUST-COOK\\\", \\\"OrgName\\\": \\\"Cookbook Customer\\\", \\\"OrgPhone\\\": \\\"555-0199\\\"}\",\n  \"parentEIRecordId\": null,\n  \"workflowInstanceId\": null,\n  \"importMode\": \"upsert\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P2.3.1 A2 — save using friendly InternalName keys in DataJson (CustomerNumber, OrgName, OrgPhone). Portal.UI still sends numeric (zero-cost fast path); integrators get one extra EntityDetail call per request to resolve names. Unknown names return 400 with the unresolved list."
     },
     "response": []
    },
    {
     "name": "Bulk Save Instances (friendly DataJson keys)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/bulk",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "bulk"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"importMode\": \"upsert\",\n  \"items\": [\n    {\n      \"key1\": \"COOK-BULK-{{$timestamp}}-1\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"COOK-1\\\", \\\"OrgName\\\": \\\"Bulk 1\\\"}\"\n    },\n    {\n      \"key1\": \"COOK-BULK-{{$timestamp}}-2\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"COOK-2\\\", \\\"OrgName\\\": \\\"Bulk 2\\\"}\"\n    },\n    {\n      \"key1\": \"COOK-BULK-{{$timestamp}}-3\",\n      \"dataJson\": \"{\\\"CustomerNumber\\\": \\\"COOK-3\\\", \\\"OrgName\\\": \\\"Bulk 3\\\"}\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P2.3.1 bulk save with friendly DataJson keys. Property map built ONCE per batch (10k rows = 1 extra DB call). Canonical-only batches skip the map entirely."
     },
     "response": []
    },
    {
     "name": "Cookbook view: OR (Carrier OR Trane) — per-rule conjunction",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/views",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "views"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"CarrierOrTraneDemo\",\n  \"name\": \"Carrier OR Trane Demo\",\n  \"viewJson\": \"{\\\"columns\\\": [{\\\"propertyInternalName\\\": \\\"EquipmentNumber\\\"}, {\\\"propertyInternalName\\\": \\\"EquipmentMake\\\"}, {\\\"propertyInternalName\\\": \\\"EquipmentModel\\\"}], \\\"filters\\\": {\\\"propertyFilters\\\": {\\\"logic\\\": \\\"OR\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Trane\\\"}], \\\"groups\\\": null}}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Saved filtered view on Equipment. Per-rule conjunction — rule[1] has conjunction='or' so it combines as OR with rule[0]. Named-key view through the view via POST /api/v1/entities/Equipment/views/{viewId}/named (renamed from /pivot on 2026-04-21 — it's a key rename, not a crosstab)."
     },
     "response": []
    },
    {
     "name": "Named-key view BY ID — /views/{viewId}/named",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/views/42/named",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "views",
        "42",
        "named"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{ \"page\": 1, \"pageSize\": 100 }",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Named-key (friendly InternalName-keyed) data scoped to a saved view's columns + filters + sort. Replace 42 with the actual EntityInstanceViewId from the views/list response. Server reads the view's canonical filters.propertyFilters and sortColumns + applies them. Use for quick integrator one-shots; for direct integrator filtering without a saved view, use /instances/list with explicit filterRules in the body instead."
     },
     "response": []
    },
    {
     "name": "Named-key view BY FRIENDLY NAME — /views/{viewIdOrName}/named",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/views/Carrier%20Locked/named",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "views",
        "Carrier%20Locked",
        "named"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{ \"page\": 1, \"pageSize\": 100 }",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same endpoint as the by-ID variant, but the {viewIdOrName} path slot accepts either a numeric EntityInstanceViewId OR a friendly view InternalName. URL-encode spaces as %20 (NOT +). The Carrier Locked view is seeded by SeedDemoFormLayouts.sql with a canonical Make=Carrier filter; the server's TryTranslateCanonicalViewFilter helper walks the saved viewJson.filters.propertyFilters → translates entity+property InternalNames to entityPropertyId via the EntityDictionary → emits the proc-target wire shape EntityInstancePivot consumes. Same dual-mode contract as elsewhere: id OR name OR both, server fills the gap."
     },
     "response": []
    },
    {
     "name": "Resolve view cross-entity — /views/resolve/{nameOrId}",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/views/resolve/CustomerAll",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "views",
        "resolve",
        "CustomerAll"
       ]
      },
      "description": "Resolve a view's nameOrId (InternalName OR numeric EntityInstanceViewId) to its canonical {entityId, entityInternalName, viewId, viewInternalName} tuple WITHOUT knowing which entity it lives under. The mobile menu drawer uses this when a menu item targets a view by name. Walks every entity in the tenant looking for a match; first match wins. Returns 404 when no view with that name or id exists. DataView role."
     },
     "response": []
    },
    {
     "name": "Named instances WITH viewId in body — /instances/named",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/named",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "named"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entityInstanceViewId\": 42,\n  \"page\": 1,\n  \"pageSize\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Alternative path that uses /instances/named with entityInstanceViewId carried in the JSON body instead of the URL. Same canonical-translator behavior — saved view's filters.propertyFilters resolves to the proc-target wire shape via the EntityDictionary. Caller-supplied filterRules in the body override the view's saved filters when both are present (parity with the WebApi PivotInstances controller logic)."
     },
     "response": []
    },
    {
     "name": "Cookbook view: AND (high-hours AND Compressor note)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/views",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "views"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"HighHoursCompressorDemo\",\n  \"name\": \"High-Hours Compressor Demo\",\n  \"viewJson\": \"{\\\"columns\\\": [{\\\"propertyInternalName\\\": \\\"ReadingDate\\\"}, {\\\"propertyInternalName\\\": \\\"OperatingHours\\\"}, {\\\"propertyInternalName\\\": \\\"Temperature\\\"}, {\\\"propertyInternalName\\\": \\\"Notes\\\"}], \\\"filters\\\": {\\\"propertyFilters\\\": {\\\"logic\\\": \\\"AND\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"EquipmentReading\\\", \\\"property\\\": \\\"OperatingHours\\\", \\\"op\\\": \\\"gt\\\", \\\"value\\\": \\\"10000\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"property\\\": \\\"Notes\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Compressor\\\"}], \\\"groups\\\": null}}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "AND between two rules — OperatingHours > 10000 AND Notes contains 'Compressor'. Regression tripwire for AND behavior alongside the OR fix."
     },
     "response": []
    },
    {
     "name": "Cookbook view: 3-way OR brands",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/views",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "views"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"ThreeBrandsDemo\",\n  \"name\": \"Three Brands Demo\",\n  \"viewJson\": \"{\\\"columns\\\": [{\\\"propertyInternalName\\\": \\\"EquipmentMake\\\"}, {\\\"propertyInternalName\\\": \\\"EquipmentModel\\\"}], \\\"sortColumns\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"direction\\\": \\\"asc\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentModel\\\", \\\"direction\\\": \\\"asc\\\"}], \\\"filters\\\": {\\\"propertyFilters\\\": {\\\"logic\\\": \\\"OR\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Trane\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"property\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Lennox\\\"}], \\\"groups\\\": null}}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Three-way OR via per-rule conjunction. Stress-tests the pivot proc's filter builder with >2 rules."
     },
     "response": []
    },
    {
     "name": "Cookbook error: save with unknown property name → 400",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"key1\": \"COOK-ERR-{{$timestamp}}\",\n  \"dataJson\": \"{\\\"ThisFieldDoesNotExist\\\":\\\"x\\\"}\",\n  \"importMode\": \"upsert\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Error shape — unknown property InternalName in DataJson returns 400 with the unresolved list + pointer to /api/v1/schema/dictionary for the authoritative vocabulary. Phase P2.3.1 A2."
     },
     "response": []
    },
    {
     "name": "Cookbook error: cross-entity key → 404",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "CUST-001"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Tenant-safety — Customer externalKey 'CUST-001' exists on Customer but not on Equipment. Returns 404 rather than silent wrong data. Resolver verifies (entity, tenant) pair before returning."
     },
     "response": []
    },
    {
     "name": "PR #325 — Siblings filter (groups[[]]): Carrier OR Trane",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"filterRules\": {\n    \"logic\": \"OR\",\n    \"rules\": [],\n    \"groups\": [\n      [\n        {\n          \"entity\": \"Equipment\",\n          \"field\": \"EquipmentMake\",\n          \"op\": \"equals\",\n          \"value\": \"Carrier\"\n        }\n      ],\n      [\n        {\n          \"entity\": \"Equipment\",\n          \"field\": \"EquipmentMake\",\n          \"op\": \"equals\",\n          \"value\": \"Trane\"\n        }\n      ]\n    ]\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "list"
       ]
      },
      "description": "PR #325 — TRUE siblings shape with `groups[[...],[...]]`. Each inner array is a sibling group; outer `logic` combines them. Cross-entity OR composition that the legacy flat shape with per-rule conjunction can't always express. Engine post-walk-filters #RawResults via JSON_VALUE accessor on CombinedPivotJson — proc auto-detects siblings via presence of `$.filters.groups`."
     },
     "response": []
    },
    {
     "name": "PR #325 — Multi-column sort (array): Make ASC, Model ASC, Serial ASC",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"sort\": [\n    {\n      \"field\": \"prop_100093\",\n      \"descending\": false,\n      \"dataTypeId\": 5\n    },\n    {\n      \"field\": \"prop_100069\",\n      \"descending\": false,\n      \"dataTypeId\": 5\n    },\n    {\n      \"field\": \"prop_100068\",\n      \"descending\": false,\n      \"dataTypeId\": 5\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "list"
       ]
      },
      "description": "PR #325 — multi-column sort via array shape. Order = ORDER BY priority. First entry primary; subsequent are tiebreakers. Single-object shape `{field, descending, dataTypeId}` still accepted (back-compat with PR #322 callers). `dataTypeId` hint drives type-aware cast: 1/11 (numeric) → DECIMAL, 2/3 (date) → DATETIME2, 4 (time) → TIME, else lex. EntityPropertyIds shown for Equipment Make/Model/Serial — replace with your tenant's ids via /schema/dictionary."
     },
     "response": []
    },
    {
     "name": "PR #325 — Named view by FRIENDLY name (URL-encoded)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/views/Carrier%20Locked/named",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "views",
        "Carrier%20Locked",
        "named"
       ]
      },
      "description": "PR #325 — dual-mode `{viewIdOrName}` route slot. Numeric id OR friendly InternalName (URL-encode spaces as `%20`, NOT `+` — path-segment encoding). Server-side translator reads view's saved `propertyFilters` + `sortColumns[]` and applies BOTH at query time (PR #325 wired sort flow alongside the existing filter flow)."
     },
     "response": []
    },
    {
     "name": "PR #327 — gte on DateOnly: ReadingDate >= 2026-04-01",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 100,\n  \"filterRules\": [\n    {\n      \"field\": \"prop_100104\",\n      \"op\": \"gte\",\n      \"value\": \"2026-04-01\",\n      \"conjunction\": \"and\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "list"
       ]
      },
      "description": "PR #327 — date-aware ordinal cast. Pre-#327, gte/lte cast LHS+RHS as DECIMAL → silent zero matches on date strings. Post-#327, the proc looks up the property's DataTypeId (3 = DateOnly here), casts as DATETIME2, comparison is correct. Same fix applies to gt/lt and to TimeOnly fields (cast as TIME). Replace `prop_100104` with your tenant's ReadingDate EntityPropertyId via /schema/dictionary."
     },
     "response": []
    },
    {
     "name": "PR #327 — SQL symbol >= auto-corrects to gte",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 100,\n  \"filterRules\": [\n    {\n      \"field\": \"prop_100104\",\n      \"op\": \">=\",\n      \"value\": \"2026-04-01\",\n      \"conjunction\": \"and\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "list"
       ]
      },
      "description": "PR #327 — operator alias auto-correction at the API boundary. `FilterOpNormalizer` maps SQL symbols (>=, <=, >, <, =, !=, <>, ==), JS/Python style (===, !==, not_equal), and canonical shorthand (eq, neq, ne, isnull, is_null, etc.) to the proc-target canonical op before the request hits the proc. Same dictionary drives /validate via the shared `Logic.Reports.Validation.OpAliases` map."
     },
     "response": []
    },
    {
     "name": "PR #327 — JS-style === auto-corrects to equals",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"filterRules\": [\n    {\n      \"field\": \"prop_100093\",\n      \"op\": \"===\",\n      \"value\": \"Carrier\",\n      \"conjunction\": \"and\"\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "list"
       ]
      },
      "description": "PR #327 — JS/Python authoring style accepted. `===` and `!==` map to equals/notEquals. Useful when LLM-driven agents generate filter JSON from natural language and emit JS-style ops. Replace `prop_100093` with your tenant's EquipmentMake EntityPropertyId."
     },
     "response": []
    },
    {
     "name": "Sync — changed-since (dirty pull for sync agents)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"sinceUtc\": \"2026-05-01T00:00:00Z\",\n  \"page\": 1,\n  \"pageSize\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/changed-since",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "changed-since"
       ]
      },
      "description": "Dirty-pull endpoint sync agents call to incremental-fetch only rows modified since their last successful checkpoint. Returns rows with `ModifiedDate > @sinceUtc`. Each response carries the agent's canonical RowHash so downstream filtering can short-circuit unchanged content. Pair with /deleted-since to also pick up tombstones."
     },
     "response": []
    },
    {
     "name": "Sync — deleted-since (tombstones for sync agents)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"sinceUtc\": \"2026-05-01T00:00:00Z\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/deleted-since",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "deleted-since"
       ]
      },
      "description": "Tombstone endpoint sync agents call to learn which rows were soft-deleted since their checkpoint. Reads hist.EntityInstance for rows where ActiveEnd transitioned from ActiveEndDate() (live sentinel) to a real timestamp. Agents apply these as DELETEs in their downstream system."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Variance & Time-Travel",
   "description": "Single-entity time-travel endpoints backed by hist.EntityInstance. Two flavors:\\n\\n**AsOf** — \"what did the whole entity look like on date X?\" Returns one row per EIRecordId with either the current dbo state (if its [ActiveStart, ActiveEnd] brackets AsOfDate) or the most recent hist snapshot active at that moment. SourceType column flags which.\\n\\n**Variance** — \"what changed on this one record over time?\" Returns chronological snapshots (hist first, current last) + per-property diffs between adjacent snapshots. ChangeKind is one of {unchanged, changed, added, removed}. Baseline snapshot (index 1) emits every prop as \"unchanged\" with OldValue NULL.\\n\\n**Entity {idOrName}** — both endpoints accept either a numeric EntityId (e.g. 100010) OR a friendly InternalName (\"Equipment\") in the route. Friendly names are resolved via EntityList and return 404 if unknown.\\n\\nRequires DataView role (or API key with that scope).",
   "item": [
    {
     "name": "AsOf: Equipment snapshot at 2026-02-01 (by EntityId)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/as-of",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "as-of"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"asOfDate\": \"2026-02-01T00:00:00Z\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Every Equipment instance's state at 2026-02-01. UNIONs live dbo.EntityInstance rows whose temporal window brackets the moment with the most recent hist.EntityInstance snapshot before it. SourceType field on each row flags 'current' vs 'hist'. Friendly entity name in URL — no tenant-local numeric IDs."
     }
    },
    {
     "name": "AsOf: Customer snapshot at 2026-01-15 (by InternalName)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/as-of",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "as-of"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"asOfDate\": \"2026-01-15T00:00:00Z\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same endpoint, but uses the friendly InternalName \"Customer\" in the route instead of the numeric EntityId. Server resolves \"Customer\" to the tenant-local EntityId via EntityList. Unknown names return 404 with a helpful error."
     }
    },
    {
     "name": "Variance: Single Equipment record (EIRecordId 12, no date filter)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/12/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "12",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Walks all history for one Equipment record — 3 snapshots with the seeded test hist. Response has { Snapshots: [...], PropertyChanges: [...] } where PropertyChanges shows EquipmentMake transitioning \"Carrier!\" → \"Carrier (was)\" → \"Carrier\" across snapshots 1→2→3 with ChangeKind=\"changed\". Replace the 12 with your EIRecordId."
     }
    },
    {
     "name": "Variance: Q1 2026 window only",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/12/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "12",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"dateFrom\": \"2026-01-01T00:00:00Z\",\n  \"dateTo\": \"2026-03-31T23:59:59Z\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same instance, but only snapshots whose ActiveStart falls in Q1 2026 are returned. Combined with includeHistory in Reports, gives integrators a full before/after audit trail for any record. Useful for compliance reports and \"who changed what when\" queries."
     }
    },
    {
     "name": "Variance: Single Equipment record by externalKey",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/EQ-4521/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "EQ-4521",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P2.3.1 — variance walk by externalKey. {idOrKey} accepts numeric EIRecordId or externalKey. Returns chronological snapshots + per-property diffs between adjacent snapshots (added / changed / removed / unchanged). Optional body: {\"dateFrom\":\"...\",\"dateTo\":\"...\"} narrows the window. Requires DataView or DataAdmin role."
     },
     "response": []
    },
    {
     "name": "Variance: RDG-00100 probe (6 snapshots, all properties)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/RDG-00100/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "RDG-00100",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P-property — demos single-entity variance against a continuous-monitor probe seeded with 6 monthly snapshots (5 hist + 1 current). Every property returns chronologically with ChangeKind per snapshot. RDG-00100 is on Carrier EQUIP-001; OperatingHours climbs 12450→17100, Temperature drifts 72.4→78.5, Notes evolve from \"baseline\" to \"service flagged\". Expected response: Snapshots[] has 6 entries, PropertyChanges[] has 30 rows (6 Ã— 5 properties) of which 20 are ChangeKind=changed and 10 unchanged."
     },
     "response": []
    },
    {
     "name": "Variance: RDG-00100 probe — Temperature only (by propertyNames)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/RDG-00100/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "RDG-00100",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyNames\": [\"Temperature\"]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P-property — narrows the variance grid to a single property on the instance. The API resolves property InternalNames to tenant-local EntityPropertyIds via EntityDetail under the hood so callers don't need to know numeric IDs. Case-insensitive. Expected: PropertyChanges[] drops from 30 rows to 6 (just Temperature across 6 snapshots). Use this to study drift on one field — most common analytical pattern."
     },
     "response": []
    },
    {
     "name": "Variance: RDG-00100 probe — multiple props (by propertyIds)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/RDG-00100/variance",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "RDG-00100",
        "variance"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"propertyIds\": [100090, 100063]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P-property — same narrow-to-properties feature but using numeric EntityPropertyIds directly (faster, skips the name→id lookup). IDs shown (100090=Temperature, 100063=OperatingHours) are the defaults for a freshly-seeded Acme tenant; yours may differ if you imported in a different order. Discover them via /variance/properties below, or GET /api/v1/entities/EquipmentReading to see the full property map. Mix propertyIds + propertyNames in the same request — the API unions them."
     },
     "response": []
    },
    {
     "name": "Variance Discovery: RDG-00100 per-property stats",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/EquipmentReading/instances/RDG-00100/variance/properties",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "EquipmentReading",
        "instances",
        "RDG-00100",
        "variance",
        "properties"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P-property — lightweight discovery endpoint. Returns one row per property on the instance with ChangeCount, DistinctValueCount, SnapshotCount, FirstChanged, LastChanged, and (for DataType Number/Currency) MinNumeric/MaxNumeric/AvgNumeric. Sorted by ChangeCount DESC so the \"most-drifted\" properties surface first — UIs use this to drive a chip picker (\"show me properties that actually changed\"). Optional body narrows with dateFrom/dateTo the same way /variance does. Expected for RDG-00100: OperatingHours (ChangeCount=6, Min=12450, Max=17100), Temperature (6, 72.4, 78.5), Notes (6 changes, no numerics), ReadingDate (6 changes, no numerics), ReadingId (1 change — initial add only)."
     },
     "response": []
    },
    {
     "name": "Entity-level AsOf — all Equipment alive at 2026-04-01",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"asOf\": \"2026-04-01T00:00:00Z\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/as-of",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "as-of"
       ]
      },
      "description": "Entity-level as-of: snapshot of an entire entity at a moment. Walks dbo + hist via UNION + window predicate `ActiveStart <= @AsOf AND ActiveEnd > @AsOf`. Different from per-instance variance (single EIRecordId chronology) — this returns the whole table at a given moment, useful for compliance reporting + integrator delta calculations."
     },
     "response": []
    },
    {
     "name": "Per-instance history — single Equipment chronology",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Equipment/instances/EQ-100/history",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Equipment",
        "instances",
        "EQ-100",
        "history"
       ]
      },
      "description": "Full chronology of one row across hist + dbo. Returns every version with ActiveStart/ActiveEnd window. Pair with /variance for change flags + values."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Reports",
   "description": "Cross-entity report endpoints. DefinitionJson drives everything: primary entity + flat joins[] array with parentEntity for chains + joinType (child|parent) + filters scoped per entity + joinMode (inner|left).\n\n**Friendly InternalName form is the default throughout this folder.** The API auto-detects whether you sent friendly names (`\"primaryEntity\":\"Customer\"`) or canonical numeric IDs (`\"primaryEntityId\":100013`) and translates on the fly. Friendly form is portable (works against any tenant that has the entity defined) and readable.\n\nTwo showcase requests — *Save Report — canonical ID form (showcase)* and *Execute Ad-Hoc — canonical ID form (showcase)* — demonstrate that the canonical numeric path is still fully supported. Those two requests reference collection variables `acmeServiceLocationEntityId` and `acmeEquipmentEntityId` carrying default Acme-seed tenant-local IDs; override them in your environment if your tenant imported entities in a different order. Run `POST /api/v1/entities/list` or `POST /api/v1/schema/dictionary` to discover your tenant's IDs.\n\nAll endpoints require DataRoles (DataView for read/execute, DataEdit for save, DataDelete for delete). API keys with those scopes authenticate identically to session tokens — same `Authorization: Bearer` header.\n\nOutput rows contain `primaryEIRecordId` + `childEIRecordId` (deepest/last join) + `combinedPivotJson` — one flattened JSON object per row with every column from every level prefixed by `{EntityInternalName}_` and per-entity `{EntityInternalName}_EIRecordId` / `{EntityInternalName}_ExternalKey` anchors.",
   "item": [
    {
     "name": "List Reports",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Returns reports visible to the caller (private + shared + API-visible). Requires DataView."
     }
    },
    {
     "name": "Save Report — canonical ID form (showcase)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"reportDefinitionId\": 0,\n  \"internalName\": \"Equipment by Location (via API)-{{$timestamp}}\",\n  \"description\": \"Every piece of equipment grouped by its service location.\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"definitionJson\": \"{\\\"primaryEntityId\\\":\\\"{{acmeServiceLocationEntityId}}\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entityId\\\":\\\"{{acmeEquipmentEntityId}}\\\",\\\"parentEntityId\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a 2-entity report: Equipment primary + parent ServiceLocation. Uses friendly primaryEntity / entity names — no tenant-local numeric IDs. internalName is timestamped so reruns always create a new row."
     }
    },
    {
     "name": "Save Report — 3-entity chain",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"ThreeEntityChainDemo-{{$timestamp}}\",\n  \"name\": \"Three-Entity Chain Demo\",\n  \"description\": \"Customer -> ServiceLocation -> Equipment filtered to Carrier units. Uses friendly entity/field names (no tenant-local numeric IDs).\",\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a 3-entity chain report using friendly entity + field names. The `internalName` gets a timestamp suffix so rerunning this sample always creates a new row instead of 409-ing."
     }
    },
    {
     "name": "Save Report — parents-as-joins",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"reportDefinitionId\": 0,\n  \"internalName\": \"Equipment With Ancestry — API-{{$timestamp}}\",\n  \"description\": \"Each Equipment row enriched with its Location and Customer via parent joins.\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Equipment\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"parent\\\"},{\\\"entity\\\":\\\"Customer\\\",\\\"parentEntity\\\":\\\"ServiceLocation\\\",\\\"joinType\\\":\\\"parent\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Save a 3-entity parents-as-joins chain using friendly InternalName strings. Equipment is the primary; ServiceLocation and Customer are walked up via joinType:\"parent\"."
     }
    },
    {
     "name": "Get Report Detail",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Get the definition of seeded report #1 ('Equipment by Location'). Replace the 1 with any ReportDefinitionId you see in /api/v1/reports/list to inspect that report's DefinitionJson."
     }
    },
    {
     "name": "Update Existing Report",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"reportDefinitionId\": 1,\n  \"internalName\": \"EquipmentByLocationSeeded\",\n  \"name\": \"Equipment by Location (seeded #1 \\u2014 update demo)\",\n  \"description\": \"Updates seeded report #1 in place (same internalName -> update). Swap reportDefinitionId to update a different report you own.\",\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"rules\\\": []}}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update an existing saved report by reportDefinitionId. Only re-run after you've actually saved a report you own; 404 if the id doesn't belong to your tenant."
     }
    },
    {
     "name": "Delete Report",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/99999",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "99999"
       ]
      },
      "description": "DELETE a report by id. The 99999 is intentional — it will 404 safely. To actually delete, replace with a real ReportDefinitionId from /api/v1/reports/list (run 'Save Report' first to create a throwaway report, then delete that).",
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      }
     }
    },
    {
     "name": "Execute Saved Report",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/1/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "1",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"pageNumber\": 1,\n  \"pageSize\": 10\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Execute seeded report #1 ('Equipment by Location'). Swap the 1 for any id from /api/v1/reports/list to run that report. Returns rows with primary/child EIRecordIds and CombinedPivotJson."
     }
    },
    {
     "name": "Execute Ad-Hoc — canonical ID form (showcase)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntityId\\\":\\\"{{acmeServiceLocationEntityId}}\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entityId\\\":\\\"{{acmeEquipmentEntityId}}\\\",\\\"parentEntityId\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Ad-hoc execute: Equipment primary + parent ServiceLocation. Friendly entity names throughout."
     }
    },
    {
     "name": "Execute Ad-Hoc — 3-entity chain + cascading filter",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Ad-hoc execute, 3-entity chain, filtered to Carrier equipment. Friendly entity/field names — no numeric IDs to memorize."
     }
    },
    {
     "name": "Execute Ad-Hoc — parents-as-joins",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Equipment\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"parent\\\"},{\\\"entity\\\":\\\"Customer\\\",\\\"parentEntity\\\":\\\"ServiceLocation\\\",\\\"joinType\\\":\\\"parent\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Execute a parents-as-joins chain ad-hoc. Starts at Equipment and walks up to Location and Customer. Friendly InternalName form."
     }
    },
    {
     "name": "Execute Ad-Hoc — pagination (page 2)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 2,\n  \"pageSize\": 5\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Pagination demo — page 2 of 5-per-page results. pageNumber/pageSize count PRIMARY rows, not output rows (a report with multi-child joins returns >pageSize output rows)."
     }
    },
    {
     "name": "HVAC: Carrier Units at CT Locations",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"field\\\": \\\"State\\\", \\\"op\\\": \\\"equals\\\", \\\"value\\\": \\\"CT\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "HVAC story: every Carrier unit at every Connecticut service location. Friendly names throughout; integrators don't need numeric IDs."
     }
    },
    {
     "name": "HVAC: Work Order Dispatch Board",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"WorkOrder\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entity\\\":\\\"WorkOrderVisit\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"},{\\\"entity\\\":\\\"Employee\\\",\\\"parentEntity\\\":\\\"WorkOrderVisit\\\",\\\"joinType\\\":\\\"parent\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Dispatch-board view: WorkOrder → WorkOrderVisit (children) → Employee (parent of each visit). Friendly form."
     }
    },
    {
     "name": "HVAC: Billy's Visits",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"WorkOrderVisit\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entity\\\":\\\"WorkOrder\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"parent\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[{\\\"entity\\\":\\\"WorkOrderVisit\\\",\\\"field\\\":\\\"externalKey\\\",\\\"op\\\":\\\"contains\\\",\\\"value\\\":\\\"EMP-001\\\"}]},\\\"sort\\\":null}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Every WorkOrderVisit by Employee EMP-001 with the parent WorkOrder context. Friendly form."
     }
    },
    {
     "name": "HVAC: Service Location Inventory",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"ServiceLocation\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"pageSize\\\":50,\\\"joins\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"columns\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"sort\\\":null}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Inventory per location: ServiceLocation primary, Equipment as children. Friendly form."
     }
    },
    {
     "name": "HVAC: Left-Join Demo (all customers even without matching equipment)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"left\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "joinMode='left' preserves primary rows even when no child matches. Customers without Carrier equipment still appear with NULL Equipment_* columns."
     }
    },
    {
     "name": "Schema Dictionary (tenant vocabulary in one call)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema/dictionary",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema",
        "dictionary"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "GET the tenant's entire schema in one request: entities, properties, parent relationships, and property options. External integrators use this to build name→ID maps before calling reports/entities endpoints. Optional ?languageRegionId=69 query param for display-name language."
     }
    },
    {
     "name": "Friendly: Customer → ServiceLocation → Equipment (Carrier)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Customer\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"},{\\\"entity\\\":\\\"Equipment\\\",\\\"parentEntity\\\":\\\"ServiceLocation\\\",\\\"joinType\\\":\\\"child\\\"}],\\\"filters\\\":{\\\"rules\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":\\\"EquipmentMake\\\",\\\"op\\\":\\\"contains\\\",\\\"value\\\":\\\"Carrier\\\"}]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same 3-entity cascade as \"HVAC: Carrier Units\" but written with InternalName strings instead of IDs. Server detects the friendly shape via root \"primaryEntity\" + non-numeric \"field\" values and translates before executing. Works identically against any tenant that has these entities defined — no ID lookup needed by the integrator."
     }
    },
    {
     "name": "Friendly: Equipment Drill-Up by Name",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Equipment\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"parent\\\"},{\\\"entity\\\":\\\"Customer\\\",\\\"parentEntity\\\":\\\"ServiceLocation\\\",\\\"joinType\\\":\\\"parent\\\"}],\\\"filters\\\":{\\\"rules\\\":[]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Parents-as-joins written with names: Equipment → ServiceLocation (parent) → Customer (parent of Location). Every Equipment row carries its full ancestry in CombinedPivotJson, no Equipment IDs required on the integrator side."
     }
    },
    {
     "name": "Aggregation: Equipment COUNT per ServiceLocation",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"ServiceLocation\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"filters\\\":{\\\"rules\\\":[]},\\\"groupBy\\\":{\\\"fields\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"field\\\":\\\"State\\\"}],\\\"aggregates\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":null,\\\"aggregate\\\":\\\"count\\\",\\\"alias\\\":\\\"EquipmentCount\\\"}]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Equipment counts grouped by ServiceLocation.State. Shows groupBy + COUNT in friendly form."
     }
    },
    {
     "name": "Aggregation: Customer rollup with COUNT + MIN/MAX warranty",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Customer\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"},{\\\"entity\\\":\\\"Equipment\\\",\\\"parentEntity\\\":\\\"ServiceLocation\\\",\\\"joinType\\\":\\\"child\\\"}],\\\"filters\\\":{\\\"rules\\\":[]},\\\"groupBy\\\":{\\\"fields\\\":[{\\\"entity\\\":\\\"Customer\\\",\\\"field\\\":\\\"CustomerNumber\\\"}],\\\"aggregates\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":null,\\\"aggregate\\\":\\\"count\\\",\\\"alias\\\":\\\"EquipmentCount\\\"},{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":\\\"WarrantyMonths\\\",\\\"aggregate\\\":\\\"min\\\",\\\"alias\\\":\\\"MinWarranty\\\"},{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":\\\"WarrantyMonths\\\",\\\"aggregate\\\":\\\"max\\\",\\\"alias\\\":\\\"MaxWarranty\\\"}]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Per-customer rollup: COUNT of equipment + MIN/MAX warranty months across all their equipment. Friendly form."
     }
    },
    {
     "name": "Aggregation: Multi-field group (Location Ã— Make) with count",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"ServiceLocation\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"filters\\\":{\\\"rules\\\":[]},\\\"groupBy\\\":{\\\"fields\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"field\\\":\\\"State\\\"},{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":\\\"EquipmentMake\\\"}],\\\"aggregates\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":null,\\\"aggregate\\\":\\\"count\\\",\\\"alias\\\":\\\"Units\\\"}]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Multi-field groupBy: count units by (State, EquipmentMake). Friendly form — no EntityPropertyId lookup needed."
     }
    },
    {
     "name": "Aggregation: Equipment by LastServiceDate (date as group key)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Equipment\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[],\\\"filters\\\":{\\\"rules\\\":[]},\\\"groupBy\\\":{\\\"fields\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":\\\"LastServiceDate\\\"}],\\\"aggregates\\\":[{\\\"entity\\\":\\\"Equipment\\\",\\\"field\\\":null,\\\"aggregate\\\":\\\"count\\\",\\\"alias\\\":\\\"UnitCount\\\"}]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Date as group key: count units grouped by LastServiceDate. Friendly form."
     }
    },
    {
     "name": "Friendly: 400 error demo — unknown entity name",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\":\\\"Customeer\\\",\\\"joinMode\\\":\\\"inner\\\",\\\"joins\\\":[{\\\"entity\\\":\\\"ServiceLocation\\\",\\\"parentEntity\\\":null,\\\"joinType\\\":\\\"child\\\"}],\\\"filters\\\":{\\\"rules\\\":[]}}\",\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Intentionally misspelled \"Customeer\" to demonstrate the translator's error response. Server returns 400 with a Levenshtein-based suggestion: \"Entity 'Customeer' not found — did you mean 'Customer'?\". Property-name typos behave identically (suggestion scoped to that entity's properties)."
     }
    },
    {
     "name": "Cookbook: Filter AND — Carrier only",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Filter single rule AND — returns every Carrier unit. Default conjunction is 'and'; listed explicitly here for clarity. Scenario 1 from the sanity battery + MSTest SavedReportScenarioTests."
     },
     "response": []
    },
    {
     "name": "Cookbook: AND chain — CUST-001 AND Carrier (intersection)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Customer\\\", \\\"field\\\": \\\"CustomerNumber\\\", \\\"op\\\": \\\"equals\\\", \\\"value\\\": \\\"CUST-001\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "AND chain + multi-entity filter. Every returned row has BOTH CUST-001 and Carrier in CombinedPivotJson. Regression tripwire — AND behavior unchanged from pre-Phase P2.3.1."
     },
     "response": []
    },
    {
     "name": "Cookbook: OR — Carrier OR Trane (both brands returned)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"or\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Trane\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Two-rule OR filter. Every Carrier AND every Trane unit. Phase P2.3.1 fix — ReportExecuteCore.sql now honors $.filters.conjunction (previously hardcoded to AND, returned 0 rows)."
     },
     "response": []
    },
    {
     "name": "Cookbook: OR 3-way — Carrier OR Trane OR Lennox",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"or\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Trane\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Lennox\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Three-rule OR stress-test — confirms the conjunction builder handles N rules correctly. Every row matches one of the three clauses."
     },
     "response": []
    },
    {
     "name": "Cookbook: OR with deadweight — Carrier OR ZZZNoSuch",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"or\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"Carrier\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"ZZZNoSuch\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "OR with an impossible second clause. Carrier rows still return — OR truth table holds. Confirms one deadweight clause doesn't silence matches from the other."
     },
     "response": []
    },
    {
     "name": "Cookbook: OR all-miss → empty (not whole table)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"or\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"ZZZBrand1\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": \\\"EquipmentMake\\\", \\\"op\\\": \\\"contains\\\", \\\"value\\\": \\\"ZZZBrand2\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "OR where every clause misses. Must return [] — a bugged OR could degenerate to WHERE TRUE and dump the whole table. Regression tripwire."
     },
     "response": []
    },
    {
     "name": "Cookbook: Numeric filter — OperatingHours > 10000",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"EquipmentReading\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"EquipmentReading\\\", \\\"field\\\": \\\"OperatingHours\\\", \\\"op\\\": \\\"greaterThan\\\", \\\"value\\\": \\\"10000\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 30\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "greaterThan operator on a numeric property. Seed data has hours 3200 to 18420 — several readings qualify."
     },
     "response": []
    },
    {
     "name": "Cookbook: GroupBy — Equipment COUNT per customer",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"groupBy\\\": {\\\"fields\\\": [{\\\"entity\\\": \\\"Customer\\\", \\\"field\\\": \\\"CustomerNumber\\\"}], \\\"aggregates\\\": [{\\\"entity\\\": \\\"Equipment\\\", \\\"field\\\": null, \\\"aggregate\\\": \\\"count\\\", \\\"alias\\\": \\\"EquipmentCount\\\"}]}, \\\"filters\\\": {\\\"rules\\\": []}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "GroupBy CustomerNumber with COUNT(*) over child Equipment. Returns exactly 3 rows (one per seeded Acme customer). Aggregate alias appears in every row's CombinedPivotJson."
     },
     "response": []
    },
    {
     "name": "Cookbook: Multi-Aggregate — Readings rollup (COUNT + SUM + AVG + MAX)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"parentEntity\\\": \\\"Equipment\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"groupBy\\\": {\\\"fields\\\": [{\\\"entity\\\": \\\"Customer\\\", \\\"field\\\": \\\"CustomerNumber\\\"}], \\\"aggregates\\\": [{\\\"entity\\\": \\\"EquipmentReading\\\", \\\"field\\\": null, \\\"aggregate\\\": \\\"count\\\", \\\"alias\\\": \\\"ReadingCount\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"field\\\": \\\"OperatingHours\\\", \\\"aggregate\\\": \\\"sum\\\", \\\"alias\\\": \\\"TotalHours\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"field\\\": \\\"OperatingHours\\\", \\\"aggregate\\\": \\\"avg\\\", \\\"alias\\\": \\\"AvgHours\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"field\\\": \\\"OperatingHours\\\", \\\"aggregate\\\": \\\"max\\\", \\\"alias\\\": \\\"MaxHours\\\"}]}, \\\"filters\\\": {\\\"rules\\\": []}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Four aggregate kinds on one groupBy. Customer → Location → Equipment → Reading chain with COUNT + SUM + AVG + MAX on OperatingHours per customer. All four aliases appear in every returned row's CombinedPivotJson."
     },
     "response": []
    },
    {
     "name": "Cookbook: Temporal — Equipment with history Q1 2026",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\", \\\"includeHistory\\\": true, \\\"dateFrom\\\": \\\"2026-01-01T00:00:00Z\\\", \\\"dateTo\\\": \\\"2026-03-31T23:59:59Z\\\"}], \\\"filters\\\": {\\\"rules\\\": []}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Per-join history inclusion. The Equipment level pulls from dbo.EntityInstance UNION ALL hist.EntityInstance within the Q1 window. Other levels stay live-only."
     },
     "response": []
    },
    {
     "name": "Cookbook: 4-Level chain — Readings for CUST-001",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Customer\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"Equipment\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"child\\\"}, {\\\"entity\\\": \\\"EquipmentReading\\\", \\\"parentEntity\\\": \\\"Equipment\\\", \\\"joinType\\\": \\\"child\\\"}], \\\"filters\\\": {\\\"conjunction\\\": \\\"and\\\", \\\"rules\\\": [{\\\"entity\\\": \\\"Customer\\\", \\\"field\\\": \\\"CustomerNumber\\\", \\\"op\\\": \\\"equals\\\", \\\"value\\\": \\\"CUST-001\\\"}]}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Customer → Location → Equipment → Reading. 4 levels deep, filtered to one customer. Every row contains CUST-001 + the full chain in CombinedPivotJson. Seed data returns â‰¥30 reading rows for CUST-001."
     },
     "response": []
    },
    {
     "name": "Cookbook: Variance mode — Equipment monthly (Q1 2026)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [], \\\"filters\\\": {\\\"rules\\\": []}, \\\"varianceMode\\\": {\\\"cadence\\\": \\\"monthly\\\", \\\"dateFrom\\\": \\\"2026-01-01T00:00:00Z\\\", \\\"dateTo\\\": \\\"2026-03-31T23:59:59Z\\\", \\\"pagingMode\\\": \\\"grouped\\\"}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Cross-entity variance — runs the definition at monthly snapshots across Q1 2026. Each row carries snapshotDate + changeFlags JSON describing per-property added/unchanged/changed/removed between adjacent snapshots. Cadences: hourly (8760/yr), daily, weekly, monthly, quarterly, annually."
     },
     "response": []
    },
    {
     "name": "Cookbook: Walk UP — Equipment with Customer ancestry",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": \"{\\\"primaryEntity\\\": \\\"Equipment\\\", \\\"joinMode\\\": \\\"inner\\\", \\\"joins\\\": [{\\\"entity\\\": \\\"ServiceLocation\\\", \\\"parentEntity\\\": null, \\\"joinType\\\": \\\"parent\\\"}, {\\\"entity\\\": \\\"Customer\\\", \\\"parentEntity\\\": \\\"ServiceLocation\\\", \\\"joinType\\\": \\\"parent\\\"}], \\\"filters\\\": {\\\"rules\\\": []}}\",\n  \"pageNumber\": 1,\n  \"pageSize\": 20\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "joinType=parent walks UP the hierarchy. Primary Equipment → parent ServiceLocation → parent-of-parent Customer. One row per Equipment, CombinedPivotJson includes its full ancestry. Inverse of the more common Customer → Location → Equipment top-down chain."
     },
     "response": []
    },
    {
     "name": "Cookbook: Error — ad-hoc with garbage definitionJson",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": true,\n  \"pageNumber\": 1,\n  \"pageSize\": 10\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Error shape — definitionJson must be a JSON string OR inline object. Passing a bool returns 400 with actionable error message. Phase P2.3.1 fix in both controllers."
     },
     "response": []
    },
    {
     "name": "PR #325 — Report multi-column sort across join chain",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"ServiceLocation\",\n    \"joinMode\": \"inner\",\n    \"joins\": [\n      {\n        \"entity\": \"Equipment\",\n        \"parentEntity\": null,\n        \"joinType\": \"child\"\n      }\n    ],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": [\n      {\n        \"entity\": \"Equipment\",\n        \"property\": \"EquipmentMake\",\n        \"direction\": \"asc\"\n      },\n      {\n        \"entity\": \"Equipment\",\n        \"property\": \"EquipmentModel\",\n        \"direction\": \"asc\"\n      }\n    ]\n  },\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "description": "PR #325 — `$.sort` accepts multi-column array on cross-entity reports. Each level is `{entity, property, direction, entityId?, entityPropertyId?}`. Names + ids both work (translator resolves either; ids win on conflict). Proc applies type-aware cast per level (DECIMAL for numbers, DATETIME2 for dates, TIME for time-only, lex for text). Sort columns reference the join chain's `{Entity}_{Property}` keys in CombinedPivotJson. The legacy single-object shape still accepted (PR #324 back-compat)."
     },
     "response": []
    },
    {
     "name": "PR #325 — Report siblings filter (cross-entity OR)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Equipment\",\n    \"joinMode\": \"inner\",\n    \"joins\": [],\n    \"filters\": {\n      \"logic\": \"OR\",\n      \"rules\": [],\n      \"groups\": [\n        [\n          {\n            \"entity\": \"Equipment\",\n            \"field\": \"EquipmentMake\",\n            \"op\": \"equals\",\n            \"value\": \"Carrier\"\n          }\n        ],\n        [\n          {\n            \"entity\": \"Equipment\",\n            \"field\": \"EquipmentMake\",\n            \"op\": \"equals\",\n            \"value\": \"Trane\"\n          }\n        ]\n      ]\n    }\n  },\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "description": "PR #325 — siblings shape on reports. Top-level `rules` (implicit GroupIdx=0) + each `groups[i]` form siblings. Outer `logic` combines them. Engine post-walk-filters #RawResults via JSON_VALUE on CombinedPivotJson. Cross-entity OR composition this enables cannot be expressed with the legacy flat single-conjunction shape. Variance + siblings is FIRST-CLASS too — same predicate applies to #VarianceRaw via dynamic-SQL DELETE per snapshot."
     },
     "response": []
    },
    {
     "name": "PR #325 — Report variance + siblings filter",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Equipment\",\n    \"joinMode\": \"inner\",\n    \"joins\": [],\n    \"filters\": {\n      \"logic\": \"OR\",\n      \"rules\": [],\n      \"groups\": [\n        [\n          {\n            \"entity\": \"Equipment\",\n            \"field\": \"EquipmentMake\",\n            \"op\": \"equals\",\n            \"value\": \"Carrier\"\n          }\n        ],\n        [\n          {\n            \"entity\": \"Equipment\",\n            \"field\": \"EquipmentMake\",\n            \"op\": \"equals\",\n            \"value\": \"Trane\"\n          }\n        ]\n      ]\n    },\n    \"varianceMode\": {\n      \"snapshots\": [\n        \"2026-05-03T00:00:00Z\"\n      ],\n      \"pagingMode\": \"flat\",\n      \"includeValues\": false\n    }\n  },\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "description": "PR #325 — variance + siblings is first-class. The post-walk filter pattern applies the SAME canonical-FilterGroup expression to #VarianceRaw per snapshot (not lossy fallback). Each snapshot's row stream gets narrowed to rows matching the grouped predicate; ChangeFlags then computes across the filtered sets. Acme has only currently-live equipment so this returns the Carrier+Trane subset at the chosen snapshot moment."
     },
     "response": []
    },
    {
     "name": "PR #327 — Report with date-aware gte (post-#327)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"EquipmentReading\",\n    \"joinMode\": \"inner\",\n    \"joins\": [],\n    \"filters\": {\n      \"rules\": [\n        {\n          \"entity\": \"EquipmentReading\",\n          \"field\": \"ReadingDate\",\n          \"op\": \"gte\",\n          \"value\": \"2026-04-01\"\n        }\n      ]\n    }\n  },\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "description": "PR #327 — gte on DateOnly fields works correctly inside reports. The post-walk filter on #RawResults resolves the property's DataTypeId from @PropertyMap and uses DATETIME2 cast for the comparison. Pre-#327 this returned zero rows silently."
     },
     "response": []
    },
    {
     "name": "Reports — /reports/validate (dry-run a DefinitionJson)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Customer\",\n    \"joinMode\": \"inner\",\n    \"joins\": [\n      {\n        \"entity\": \"ServiceLocation\",\n        \"parentEntity\": null,\n        \"joinType\": \"child\"\n      }\n    ],\n    \"filters\": {\n      \"rules\": [\n        {\n          \"entity\": \"ServiceLocation\",\n          \"field\": \"Stat\",\n          \"op\": \"eq\",\n          \"value\": \"CT\"\n        }\n      ]\n    }\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "validate"
       ]
      },
      "description": "Validate a report's DefinitionJson before saving + running. Returns `{valid, issues[]}` envelope. Each issue carries a `path` (e.g. `joins[0].entity`, `filters.rules[0].field`), `severity`, `message`, and Levenshtein-suggested `suggestion` for typos. Integrators use this to build forgiving authoring UX. Same `OpAliases` shared map drives this validator (PR #327) — SQL-symbol authored ops pass cleanly."
     },
     "response": []
    },
    {
     "name": "Reports — /reports/template (skeleton DefinitionJson scaffold)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema/report-template",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema",
        "report-template"
       ]
      },
      "description": "Returns an empty DefinitionJson with the canonical shape pre-filled — primaryEntity, joinMode, joins[], columns[], filters{rules,groups,logic}, sortColumns[], groupBy{fields,aggregates}, varianceMode. Integrators fork this as a starting point. Pair with `/reports/{id}/template` to fork from an existing saved report."
     },
     "response": []
    },
    {
     "name": "Reports — get a saved report's DefinitionJson (clone/edit source)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/{{reportId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "{{reportId}}"
       ]
      },
      "description": "Read a saved report's DefinitionJson (the shape to clone or edit). POST /api/v1/reports/{id} returns the stored definition; pair with POST /api/v1/schema/report-template for a blank scaffold and POST /api/v1/reports/validate to check edits before saving."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Phase P2 — Variance Reports",
   "description": "Cross-entity variance mode — run the SAME report at N historical moments and emit ChangeFlags per row describing what shifted between adjacent snapshots. Sits on top of existing Phase N/O/Q machinery (friendly API, per-join history, aggregation) and composes cleanly with all of them.\\n\\n**DefinitionJson gains a varianceMode block:**\\n```json\\n\"varianceMode\": {\\n  \"cadence\": \"hourly|daily|weekly|monthly|quarterly|annually\",  // or...\\n  \"snapshots\": [\"2026-01-15\", \"2026-03-15\", \"2026-06-15\"],      // explicit wins\\n  \"dateFrom\": \"2026-01-01\", \"dateTo\": \"2026-06-01\",             // used with cadence\\n  \"includeValues\": true,     // false (default) = flat status; true = { status, from, to }\\n  \"pagingMode\": \"grouped\"    // \"grouped\" (default) pages by leaf pair; \"flat\" by snapshot\\n}\\n```\\n\\n**Output shape adds two columns:** `SnapshotDate` (DATETIME2) and `ChangeFlags` (JSON). SnapshotDate = the as-of moment this row represents. ChangeFlags = per-property diff vs. the prior snapshot for the same (PrimaryEIRecordId, ChildEIRecordId) pair, plus an `_rowStatus` meta key with one of {added, unchanged, changed}.\\n\\n**Safety rail:** cadence derivations are capped at 500 snapshots. A daily over 2 years = 730 attempts → only the first 500 are emitted. Use explicit snapshots or a narrower window.\\n\\nRequires BuilderAdmin role (API keys with that scope accepted via Bearer).",
   "item": [
    {
     "name": "Variance: Equipment monthly cadence (grouped, flat flags)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Equipment\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"monthly\",\n      \"dateFrom\": \"2026-01-01T00:00:00\",\n      \"dateTo\": \"2026-06-01T00:00:00\",\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Six monthly snapshots across Jan–Jun 2026 on Equipment. Returns ~32 rows (4 data-having snapshots Ã— ~8 equipment units). ChangeFlags is flat status strings (\"unchanged\"/\"changed\"/\"added\"/\"removed\") per property. _rowStatus tells you the row-level verdict (first appearance = \"added\", subsequent identical = \"unchanged\", difference detected = \"changed\"). Pairs with the saved demo \"Equipment Monthly Variance\"."
     }
    },
    {
     "name": "Variance: Equipment weekly with includeValues (from/to objects)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Equipment\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"weekly\",\n      \"dateFrom\": \"2026-02-01T00:00:00\",\n      \"dateTo\": \"2026-03-01T00:00:00\",\n      \"includeValues\": true,\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same proc but with includeValues=true so each ChangeFlags entry is an object `{\"status\":\"changed\",\"from\":\"Carrier!\",\"to\":\"Carrier\"}` instead of a flat string. This is the shape the UI uses to render cell diffs — each changed cell carries its own before/after without cross-referencing a prior snapshot's CombinedPivotJson. Payload is larger but self-contained. Weekly cadence over Feb 2026 (5 snapshots)."
     }
    },
    {
     "name": "Variance: Customer explicit snapshots (audit/quarter cutoffs)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Customer\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"snapshots\": [\n        \"2026-01-15T00:00:00\",\n        \"2026-03-15T00:00:00\",\n        \"2026-06-15T00:00:00\"\n      ],\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Three hand-picked snapshot dates via the `snapshots` array rather than a cadence. The explicit array always wins over cadence if both are present. Use this when integrators care about specific as-of moments — end-of-quarter reporting, audit snapshots, compliance windows. Snapshots can be irregular. Entries outside any data window emit zero rows for that snapshot (genuinely empty, not an error)."
     }
    },
    {
     "name": "Variance: 4-level chain monthly, FLAT paging (export shape)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Customer\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [\n      {\n        \"entity\": \"ServiceLocation\",\n        \"parentEntity\": null,\n        \"joinType\": \"child\"\n      },\n      {\n        \"entity\": \"Equipment\",\n        \"parentEntity\": \"ServiceLocation\",\n        \"joinType\": \"child\"\n      },\n      {\n        \"entity\": \"EquipmentReading\",\n        \"parentEntity\": \"Equipment\",\n        \"joinType\": \"child\"\n      }\n    ],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"monthly\",\n      \"dateFrom\": \"2026-01-01T00:00:00\",\n      \"dateTo\": \"2026-04-01T00:00:00\",\n      \"pagingMode\": \"flat\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Combines Phase R's 4-level walk-up (Customer → ServiceLocation → Equipment → EquipmentReading) with Phase P2 variance + pagingMode=flat. Rows come back ordered by SnapshotDate rather than grouped by leaf pair. Best for bulk export, data pipelines, and any consumer that wants snapshots interleaved with OFFSET/FETCH paging. Showcases composition of deep chains with variance."
     }
    },
    {
     "name": "Variance: Daily cadence, hits 500-snapshot safety cap",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Customer\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": []\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"daily\",\n      \"dateFrom\": \"2024-01-01T00:00:00\",\n      \"dateTo\": \"2026-01-01T00:00:00\",\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Intentionally exercises the 500-snapshot hard cap. A daily cadence over 2 years would try 730 snapshots; the proc stops at 500 and runs with what it has. Response is NOT an error — it's a partial result. Use this sample to verify integrators know to narrow windows or switch to weekly/monthly cadence for long ranges. In production, log a warning client-side when dateTo - dateFrom exceeds the cap for the chosen cadence."
     }
    },
    {
     "name": "Variance: combined with filter (only Carrier equipment, monthly)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"Equipment\",\n    \"joinMode\": \"inner\",\n    \"pageSize\": 50,\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": {\n      \"rules\": [\n        {\n          \"entity\": \"Equipment\",\n          \"field\": \"EquipmentMake\",\n          \"op\": \"contains\",\n          \"value\": \"Carrier\"\n        }\n      ]\n    },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"monthly\",\n      \"dateFrom\": \"2026-01-01T00:00:00\",\n      \"dateTo\": \"2026-06-01T00:00:00\",\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "varianceMode composes with filters. Here: only Equipment with Make containing \"Carrier\" across 6 monthly snapshots. The filter is applied at each snapshot independently, so if an equipment's Make was NOT Carrier in an earlier snapshot but IS now, it'll appear with _rowStatus=\"added\" on the snapshot where the filter first matched. Useful for auditing \"when did this record qualify for the filter?\""
     }
    },
    {
     "name": "Variance: includeProperties (narrow cross-entity variance)",
     "request": {
      "method": "POST",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/reports/execute",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "reports",
        "execute"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"definitionJson\": {\n    \"primaryEntity\": \"EquipmentReading\",\n    \"joinMode\": \"inner\",\n    \"joins\": [],\n    \"columns\": [],\n    \"filters\": { \"rules\": [] },\n    \"sort\": null,\n    \"varianceMode\": {\n      \"cadence\": \"monthly\",\n      \"dateFrom\": \"2026-01-01T00:00:00\",\n      \"dateTo\": \"2026-06-01T00:00:00\",\n      \"includeProperties\": [100090, 100063],\n      \"pagingMode\": \"grouped\"\n    }\n  },\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Phase P-property — cross-entity variance narrowed to specific properties via varianceMode.includeProperties (JSON array of numeric EntityPropertyIds). When set, the per-level pivot emits only those properties in CombinedPivotJson; all other variance plumbing (snapshots, change flags, paging) is unchanged. IDs shown target Acme's EquipmentReading.Temperature (100090) + EquipmentReading.OperatingHours (100063) — your tenant's IDs may differ if you seeded entities in a different order. Discover them via POST /api/v1/entities/EquipmentReading. Empty/omitted = compare every column (default, backward compatible). Use when partners only care about drift on a subset of fields — tighter output payload, faster export."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Forms API — Records",
   "description": "Phase 3.1 + 3.2a — public v1 surface for a saved EntityFormLayout. Same endpoints work for view-backed AND report-backed forms. {formIdOrName} accepts numeric id OR friendly InternalName (e.g. CustomerBasicForm).\n\nEvery endpoint POST except DELETE, matching the platform's no-GET cache-safe contract. Integrators hit /schema once to cache the form's shape, then use /records/list + /records/{idOrKey} for reads and POST/PUT/DELETE for writes.\n\nWrite endpoints honor the form's Layout.AllowNew / AllowEdit / AllowDelete as hard 403 gates (regardless of caller's role), and filter-as-lock is enforced server-side: omit a locked field → server injects; send a conflicting value → 422.\n\n---\n\n## Authoring forms — the `LayoutJson` shape primer\n\nThe folder above is the **records** surface (data rows through a form). A form is also **authored** over HTTP via the *authoring* surface — see the **\"Create a Form (authoring — LayoutJson over HTTP)\"** request in this folder. A form definition is one `LayoutJson` **string** (it deserializes to `Logic.Forms.Models.FormLayout`). Author in the **friendly shape** (entity/property by `InternalName`); the canonical shape (`entityId`/`entityPropertyId`) is also accepted and normalized.\n\n### Skeleton\n\n```jsonc\n{\n  \"version\": 1,                       // int — use 1\n  \"mode\": \"single\",                   // \"single\" (one page) | \"wizard\" (stepped)\n  \"grid\": { \"columns\": 24 },          // virtual canvas width fields anchor into; 1-96\n  \"compatibility\": [\"web\",\"mobile\"],  // channel subset; null/omit = all. web,mobile,tablet,print,rbm,rcs,amb,sms,email\n  \"allowNew\": false,                  // hard create gate (403 when false, ANY role) — set all three explicitly\n  \"allowEdit\": true,                  // hard update gate\n  \"allowDelete\": false,               // hard delete gate\n  \"storageShortcutId\": null,          // form-level default storage destination for binary fields (cascade: field>section>form>property>tenant)\n  \"storageSubFolder\": null,           // sub-folder template; tokens {PrimaryEntity} / {PrimaryExternalKey}\n  \"sections\": [                       // REQUIRED, >= 1\n    {\n      \"internalName\": \"visit\",        // REQUIRED, unique within the form — also the multi-save key\n      \"title\": \"Visit\",               // display heading\n      \"order\": 0,                     // render order\n      \"sectionType\": \"detail\",        // \"detail\" (field grid, one record) | \"list\" (child-row table, master-detail)\n      \"entityBinding\": \"primary\",     // \"primary\"/null = form's entity; an entity InternalName = related/child (list sections)\n      \"parentSectionKey\": null,       // names another section to scope under (master-detail)\n      \"onMissing\": \"readonly\",        // list sections: \"readonly\" | \"allowCreate\"\n      \"listColumns\": null,            // list sections: property InternalNames shown as columns\n      \"fields\": [\n        {\n          \"entity\": \"WorkOrderVisit\", // property's entity InternalName (omit = section/form primary)\n          \"property\": \"VisitStatus\",  // property InternalName (REQUIRED unless columnKey set on report-backed forms)\n          \"c\": 0, \"r\": 0, \"w\": 8, \"h\": 1,   // grid coords: column, row (0-based), width, height. c + w <= grid.columns\n          \"labelPosition\": \"top\",     // \"top\" | \"left\" | \"hidden\"\n          \"required\": true,           // FORM-boundary required check (use for binary fields you want mandatory)\n          \"readonly\": false,\n          \"renderMode\": \"dropdown\",   // \"text\" (default) | \"dropdown\" (hard <select>) | \"combo\" (autocomplete + free type)\n          \"controlVariant\": \"select\", // visual variant of the default control; renderMode wins over this. e.g. textarea, radio\n          \"dataSource\": null,         // REQUIRED when renderMode is dropdown/combo AND the property isn't a Choice\n          \"allowNewValues\": false,    // combo only: accept a typed value not in the feed\n          \"helpText\": \"...\",          // inline help\n          \"placeholder\": null,\n          \"visibleWhen\": null,        // conditional visibility — a FilterGroup; hidden fields skip required-check\n          \"storageShortcutId\": null   // field-level binary storage override (narrowest in the cascade)\n        }\n      ]\n    }\n  ]\n}\n```\n\n### Render modes & the Choice-dropdown shortcut (what this folder's example uses)\n\n`renderMode` routes to a control: `\"text\"`/absent = the DataType default; `\"dropdown\"` = hard `<select>`; `\"combo\"` = autocomplete that also accepts free typing (Text/Number/Currency only — **combo on a Choice is rejected**). A `dataSource` is **required** for dropdown/combo **except** on a **Choice property**, whose own `PropertyOption` set is the implicit `propertyOptions` feed — so `VisitStatus` just sets `\"renderMode\":\"dropdown\"` with **no** `dataSource`. Writes accept the option's `InternalName` *or* its localized display name.\n\n`dataSource` (when you need one) is discriminated by `type`: `\"view\"` (viewId + keyColumn/displayColumn), `\"entity\"` (entityId + keyColumn/displayColumn), `\"static\"` (inline `staticItems[]` with per-item `translations`), `\"propertyOptions\"` (another Choice property's option set). `maxItems` caps the feed (default 500).\n\n### DataType ids (no Boolean — model yes/no as a Choice)\n\n`1` Number · `2` DateTime · `3` DateOnly · `4` TimeOnly · `5` Text · `6` Choice · `7` Attachment · `8` Signature · `9` Hierarchy · `10` Binary · `11` Currency. Binary-capable: 7 / 8 / 10 — reference uploaded assets as `{ \"key\": \"...\", \"name\": \"...\" }`, never raw bytes.\n\n### Conditional visibility (`visibleWhen`) — same canonical FilterGroup as views/reports\n\n```jsonc\n{ \"logic\": \"AND\", \"rules\": [ { \"entity\": \"WorkOrder\", \"property\": \"Status\", \"op\": \"eq\", \"value\": \"Cancelled\" } ], \"groups\": null }\n```\nOps: `eq, neq, gt, lt, gte, lte, between, before, after, contains, startsWith, endsWith, in, isempty, isnotempty`. A hidden field is treated as optional on save.\n\n> This is the first of a repeatable pattern: a JSON-shaped authoring surface gets its **shape skeleton at the folder level** (here) plus one fully-annotated worked request (the Create-a-Form request). Reports and other JSON surfaces follow the same layout.",
   "item": [
    {
     "name": "Universal {ok, issues[]} envelope reference",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema/whoami",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema",
        "whoami"
       ]
      },
      "description": "**Reference — not an actual save call.** Pinned at the top of the Forms folder to document the envelope every save / delete / validate endpoint returns.\n\n**On success:**\n```json\n{\n  \"ok\": true,\n  \"httpStatus\": 200,\n  \"id\": 7,\n  \"layoutVersion\": 5,\n  \"issues\": []\n}\n```\n\n**On failure:**\n```json\n{\n  \"ok\": false,\n  \"httpStatus\": 422,\n  \"id\": null,\n  \"layoutVersion\": null,\n  \"issues\": [\n    {\n      \"path\": \"CustomerNumber\",\n      \"severity\": \"error\",\n      \"message\": \"Field 'CustomerNumber' is part of the composite key and cannot be changed on update. Composite-key fields are immutable \\u2014 omit them from update payloads (the row's existing identity is preserved automatically).\",\n      \"suggestion\": null\n    }\n  ]\n}\n```\n\n**HTTP status mapping:**\n- 200 — Ok / save succeeded\n- 400 — Malformed (unknown field, bad shape)\n- 401 — Unauthorized (account locked / token rejected)\n- 403 — Forbidden (form's allow-gate denies the verb)\n- 404 — Record / form / view / report not found\n- 409 — Conflict (FK reference blocks delete; duplicate unique key)\n- 422 — Unprocessable (validation rules rejected)\n- 500 — Server error (proc returned an unexpected code)\n\nEach `issues[]` entry has `path` (JSON-path-style location), `severity` (`error` or `warning`), `message` (human-readable), and `suggestion` (optional Levenshtein 'did you mean')."
     },
     "response": []
    },
    {
     "name": "Describe form (schema)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/schema",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "schema"
       ]
      },
      "description": "Returns the form's rich self-description: metadata, field list with DataType + Required + LockedByFilter markers, Choice option lists, and runnable example create payloads. Cache the response once per form — the schema is small + doesn't change per-request. Use this to auto-build integrations (MCP tools, Zapier triggers, OpenAPI clients) against the form's exact shape."
     },
     "response": [
      {
       "name": "200 OK — CustomerBasicForm schema",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"form\": {\n    \"id\": 1,\n    \"internalName\": \"CustomerBasicForm\",\n    \"description\": \"Starter Customer form.\",\n    \"entity\": \"Customer\",\n    \"entityId\": 100013,\n    \"allowNew\": true,\n    \"allowEdit\": true,\n    \"allowDelete\": false,\n    \"isShared\": true,\n    \"isSystem\": false,\n    \"isApiVisible\": true,\n    \"binding\": { \"type\": \"view\", \"id\": 9, \"name\": \"All Customers\", \"entities\": null }\n  },\n  \"fields\": [\n    { \"internalName\": \"CustomerNumber\", \"entityPropertyId\": 100090, \"columnKey\": null, \"entity\": \"Customer\", \"label\": \"Customer Number\", \"dataType\": { \"id\": 5, \"name\": \"Text\" }, \"required\": true, \"readOnly\": false, \"readOnlyReason\": null, \"keyPosition\": 1, \"lockedByFilter\": false, \"lockedValue\": null, \"placeholder\": null, \"helpText\": \"Business identifier — key field, immutable after create.\", \"options\": null },\n    { \"internalName\": \"OrgName\",        \"entityPropertyId\": 100091, \"columnKey\": null, \"entity\": \"Customer\", \"label\": \"Org Name\",        \"dataType\": { \"id\": 5, \"name\": \"Text\" }, \"required\": true, \"readOnly\": false, \"readOnlyReason\": null, \"keyPosition\": null, \"lockedByFilter\": false, \"lockedValue\": null, \"placeholder\": null, \"helpText\": null, \"options\": null },\n    { \"internalName\": \"OrgPhone\",       \"entityPropertyId\": 100092, \"columnKey\": null, \"entity\": \"Customer\", \"label\": \"Org Phone\",       \"dataType\": { \"id\": 5, \"name\": \"Text\" }, \"required\": false, \"readOnly\": false, \"readOnlyReason\": null, \"keyPosition\": null, \"lockedByFilter\": false, \"lockedValue\": null, \"placeholder\": null, \"helpText\": null, \"options\": null }\n  ],\n  \"examples\": {\n    \"create\":     { \"CustomerNumber\": \"example CustomerNumber\", \"OrgName\": \"example OrgName\", \"OrgPhone\": \"example OrgPhone\" },\n    \"bulkCreate\": { \"records\": [ { \"CustomerNumber\": \"example CustomerNumber\", \"OrgName\": \"example OrgName\", \"OrgPhone\": \"example OrgPhone\" }, { \"CustomerNumber\": \"example CustomerNumber\", \"OrgName\": \"example OrgName\", \"OrgPhone\": \"example OrgPhone\" } ] }\n  }\n}"
      },
      {
       "name": "200 OK — CTOnlyServiceLocationForm schema (State locked)",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"form\": {\n    \"id\": 3,\n    \"internalName\": \"CTOnlyServiceLocationForm\",\n    \"entity\": \"ServiceLocation\",\n    \"allowNew\": true, \"allowEdit\": true, \"allowDelete\": false,\n    \"binding\": { \"type\": \"view\", \"id\": 11, \"name\": \"CT Service Locations\", \"entities\": null }\n  },\n  \"fields\": [\n    { \"internalName\": \"State\", \"entity\": \"ServiceLocation\", \"dataType\": { \"id\": 5, \"name\": \"Text\" }, \"required\": true, \"readOnly\": true, \"readOnlyReason\": \"lockedByFilter\", \"lockedByFilter\": true, \"lockedValue\": \"CT\", \"helpText\": \"Locked to CT via the form's bound view filter.\", \"options\": null }\n  ]\n}"
      }
     ]
    },
    {
     "name": "List records (baseline, no filter)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "list"
       ]
      },
      "description": "Paged record list through a form. The form's filter-as-lock (bound view's eq rules) auto-applies; the response's lockedFields map echoes what got auto-applied so you can render a 'filtered to X' badge without re-parsing. View-backed rows carry friendly property InternalName keys in data; report-backed rows carry {Entity}_{Property} CombinedPivotJson keys."
     },
     "response": [
      {
       "name": "200 OK — 3 Acme customers",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"records\": [\n    { \"eiRecordId\": 1, \"externalKey\": \"CUST-001\", \"parentEIRecordId\": null, \"data\": { \"CustomerNumber\": \"CUST-001\", \"OrgName\": \"Eastern Plaza Holdings\", \"OrgPhone\": \"555-1001\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" },\n    { \"eiRecordId\": 2, \"externalKey\": \"CUST-002\", \"parentEIRecordId\": null, \"data\": { \"CustomerNumber\": \"CUST-002\", \"OrgName\": \"Metro Medical Group\",   \"OrgPhone\": \"555-1002\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" },\n    { \"eiRecordId\": 3, \"externalKey\": \"CUST-003\", \"parentEIRecordId\": null, \"data\": { \"CustomerNumber\": \"CUST-003\", \"OrgName\": \"Sunbelt Real Estate\",    \"OrgPhone\": \"555-1003\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" }\n  ],\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"lockedFields\": null\n}"
      }
     ]
    },
    {
     "name": "List records — filter-as-lock demo (CT only)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CTOnlyServiceLocationForm/records/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CTOnlyServiceLocationForm",
        "records",
        "list"
       ]
      },
      "description": "CTOnlyServiceLocationForm is bound to a view filtered State=CT. lockedFields in the response echoes { State: CT }; only CT ServiceLocations appear in records[]. Filter-as-lock is enforced on BOTH list AND write paths."
     },
     "response": [
      {
       "name": "200 OK — 2 CT rows + lockedFields echo",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"records\": [\n    { \"eiRecordId\": 101, \"externalKey\": \"CUST-001 / LOC-001\", \"data\": { \"CustomerNumber\": \"CUST-001\", \"LocationNumber\": \"LOC-001\", \"City\": \"Hartford\", \"State\": \"CT\", \"ZipCode\": \"06103\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" },\n    { \"eiRecordId\": 103, \"externalKey\": \"CUST-001 / LOC-003\", \"data\": { \"CustomerNumber\": \"CUST-001\", \"LocationNumber\": \"LOC-003\", \"City\": \"Stamford\", \"State\": \"CT\", \"ZipCode\": \"06901\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" }\n  ],\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"lockedFields\": { \"State\": \"CT\" }\n}"
      }
     ]
    },
    {
     "name": "List records — report-backed (Carrier chain)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CarrierChainForm/records/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CarrierChainForm",
        "records",
        "list"
       ]
      },
      "description": "Report-backed form — records[].data carries flat {Entity}_{Property} columnKey shape (Customer_OrgName, ServiceLocation_City, Equipment_EquipmentMake, etc.) matching the report's CombinedPivotJson. Primary entity is Customer; child entities (ServiceLocation, Equipment) are read-only in 3.2 but visible as context."
     },
     "response": []
    },
    {
     "name": "Get single record by externalKey",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "CUST-001"
       ]
      },
      "description": "Fetch one record by EIRecordId (numeric) OR externalKey (friendly). Cross-entity/cross-form id reuse returns 404 — never silent wrong-row routing."
     },
     "response": []
    },
    {
     "name": "Create record",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"payload\": {\n    \"CustomerNumber\": \"CUST-042\",\n    \"OrgName\": \"Example Corp\",\n    \"OrgPhone\": \"555-0142\"\n  }\n}"
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records"
       ]
      },
      "description": "Smart payload: `CustomerNumber` is the composite-key field; `OrgName` and `OrgPhone` are regular properties. Wrapped in `payload` so we can pin `expectedLayoutVersion` for optimistic concurrency. Omit `expectedLayoutVersion` for backward-compat — the server skips the check when it's absent."
     },
     "response": [
      {
       "name": "200 OK — created",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"eiRecordId\": 42\n}"
      },
      {
       "name": "422 Unprocessable — locked-value conflict",
       "status": "Unprocessable Entity",
       "code": 422,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"Field 'State' is locked to 'CT' by the form's bound view filter; cannot send 'NY'.\",\n  \"issues\": [\n    { \"path\": \"State\", \"severity\": \"error\", \"message\": \"Field 'State' is locked to 'CT' by the form's bound view filter; cannot send 'NY'.\", \"suggestion\": null }\n  ]\n}"
      },
      {
       "name": "409 Conflict — composite key duplicate",
       "status": "Conflict",
       "code": 409,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"Composite key already exists for this entity.\"\n}"
      }
     ]
    },
    {
     "name": "Create record — child-entity form via _parentKey1 metadata",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"CustomerNumber\": \"CUST-001\",\n  \"LocationNumber\": \"LOC-PKR-{{$timestamp}}\",\n  \"City\": \"Hartford\",\n  \"State\": \"CT\",\n  \"_parentKey1\": \"CUST-001\"\n}"
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/ServiceLocationBasicForm/records",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "ServiceLocationBasicForm",
        "records"
       ]
      },
      "description": "Forms API record CREATE on a child-entity form (ServiceLocation). Parent linkage via underscore-prefixed metadata fields the server strips before validation: _parentEIRecordId (numeric pin), _parentKey1/2/3 (composite-key resolution), or _parentEntityId (optional pin to a specific entity). Without these, a form bound to a child entity would silently create orphan rows. PR #249 added ExtractParentContext to close the gap on both API.WebApi and API.Functions."
     },
     "response": []
    },
    {
     "name": "Update record (partial)",
     "request": {
      "method": "PUT",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"OrgPhone\": \"555-NEW-NUMBER\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "CUST-001"
       ]
      },
      "description": "Partial update — only fields in the payload are touched. Omitted fields keep their existing values. Enforces Layout.AllowEdit. Composite keys are immutable after create; attempts to change them are rejected by the underlying save proc."
     },
     "response": []
    },
    {
     "name": "Delete record",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/CUST-042",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "CUST-042"
       ]
      },
      "description": "Soft-delete — sets ActiveEnd to now, row becomes invisible to active queries but preserved in hist.EntityInstance for audit. Enforces Layout.AllowDelete. Returns 409 if child records reference this row (detach or delete children first)."
     },
     "response": []
    },
    {
     "name": "Validate record (dry-run)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"payload\": {\n    \"CustomerNumber\": \"CUST-999\",\n    \"OrgName\": \"Draft Corp\"\n  },\n  \"isUpdate\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "validate"
       ]
      },
      "description": "Dry-run validate — returns the standard { valid, issues[{ path, severity, message, suggestion }] } envelope without writing. Integrators gate their UI save-submit on this. Catches unknown fields, missing-required, type mismatches, locked-value conflicts, Choice option mismatches. Always returns 200 — inspect the envelope, not the HTTP status."
     },
     "response": [
      {
       "name": "200 — valid payload",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"valid\": true,\n  \"issues\": []\n}"
      },
      {
       "name": "200 — invalid with suggestions",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"valid\": false,\n  \"issues\": [\n    { \"path\": \"OrgNam\",     \"severity\": \"error\", \"message\": \"Unknown field 'OrgNam' on form 'CustomerBasicForm'.\", \"suggestion\": \"OrgName\" },\n    { \"path\": \"CustomerNumber\", \"severity\": \"error\", \"message\": \"Required field 'CustomerNumber' is missing.\", \"suggestion\": null }\n  ]\n}"
      }
     ]
    },
    {
     "name": "Multi-section save — create primary + child rows atomically",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerHierarchyForm/records/multi",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerHierarchyForm",
        "records",
        "multi"
       ]
      },
      "description": "**Wave 1 atomic multi-section save.** One transaction; either every row commits or every row rolls back.\n\n**Body shape:**\n- `expectedLayoutVersion` (optional) — optimistic concurrency. Stale   callers get 422 + currentLayoutVersion echoed back so they can re-fetch   /schema and retry.\n- `primary` — the form's primary entity payload.\n- `sections.{name}.rows[]` — child rows. Each row carries `_rowIndex`   (caller-assigned ordinal) and either `_eiRecordId: null` (insert) or   an existing id (update). New child rows reference a brand-new parent   via `_parentRowIndex` before the parent has an EIRecordId.\n- `sections.{name}.deletedEIRecordIds[]` — soft-delete those rows.\n\n**Response:** `{ ok, eiRecordId, layoutVersion, sectionResults: { rows: [{ rowIndex, eiRecordId, ok }], deletedRowCount } }`.\n\nPer-section verb gates (`AllowCreate` / `AllowEdit` / `AllowDelete`) fire per row before content validates.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"primary\": {\n    \"CustomerNumber\": \"CUST-WAVE1-001\",\n    \"OrgName\": \"Acme Wave 1 Atomic Save\",\n    \"OrgPhone\": \"555-0100\"\n  },\n  \"sections\": {\n    \"Locations\": {\n      \"rows\": [\n        {\n          \"_eiRecordId\": null,\n          \"_parentRowIndex\": 0,\n          \"_rowIndex\": 0,\n          \"City\": \"Hartford\",\n          \"State\": \"CT\"\n        },\n        {\n          \"_eiRecordId\": null,\n          \"_parentRowIndex\": 0,\n          \"_rowIndex\": 1,\n          \"City\": \"New Haven\",\n          \"State\": \"CT\"\n        }\n      ]\n    }\n  }\n}"
      }
     },
     "response": []
    },
    {
     "name": "Multi-section save — update existing primary + child rows",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerHierarchyForm/records/multi/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerHierarchyForm",
        "records",
        "multi",
        "CUST-001"
       ]
      },
      "description": "Update path. The `primaryIdOrKey` route segment selects an existing primary record; section rows can mix inserts (no `_eiRecordId`), updates (existing id), and deletes (`deletedEIRecordIds[]`).\n\n**Composite-key immutability:** any KeyPosition field on an existing-row update returns 422 with a per-field issue. Identity is the row's address, not its data — DELETE + INSERT to change a key.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"primary\": {\n    \"OrgPhone\": \"555-0199\"\n  },\n  \"sections\": {\n    \"Locations\": {\n      \"rows\": [\n        {\n          \"_eiRecordId\": 42,\n          \"City\": \"Stamford\"\n        }\n      ],\n      \"deletedEIRecordIds\": [\n        99\n      ]\n    }\n  }\n}"
      }
     },
     "response": []
    },
    {
     "name": "Multi-section validate (dry-run create)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerHierarchyForm/records/multi/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerHierarchyForm",
        "records",
        "multi",
        "validate"
       ]
      },
      "description": "**Wave 1 dry-run for the multi-save shape.** Runs every check the real save runs:\n- composite-key immutability per row\n- per-section allow-gates (AllowCreate / AllowEdit / AllowDelete)\n- AllowedFileTypes whitelist on attachment / binary fields\n- AllowNewValues=false combo enforcement (every source type)\n- expectedLayoutVersion optimistic concurrency\n- required-field, type coercion, RegEx, Min/Max length, Min/Max value, MaxDecimalPlaces\n\nAlways returns 200 (or 422 only when `expectedLayoutVersion` itself is stale). Inspect `valid` + `issues[]`. Role gate: `DataView` (read-only callers can dry-run). Gate your submit button on `valid: true`.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"primary\": {\n    \"CustomerNumber\": \"CUST-VALIDATE-001\",\n    \"OrgName\": \"Dry-run validate test\"\n  },\n  \"sections\": {\n    \"Locations\": {\n      \"rows\": [\n        {\n          \"_eiRecordId\": null,\n          \"_parentRowIndex\": 0,\n          \"_rowIndex\": 0,\n          \"City\": \"Hartford\"\n        }\n      ]\n    }\n  }\n}"
      }
     },
     "response": []
    },
    {
     "name": "Multi-section validate (dry-run update)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerHierarchyForm/records/multi/CUST-001/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerHierarchyForm",
        "records",
        "multi",
        "CUST-001",
        "validate"
       ]
      },
      "description": "Same as the create dry-run but anchored to an existing primary record. Useful for previewing the outcome of an update batch before the user clicks Save — surfaces composite-key violations on existing-row updates with a path like `sections.Locations.rows[0].LocationNumber`.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"primary\": {\n    \"OrgPhone\": \"555-0199\"\n  },\n  \"sections\": {\n    \"Locations\": {\n      \"rows\": [\n        {\n          \"_eiRecordId\": 42,\n          \"City\": \"Stamford\"\n        }\n      ],\n      \"deletedEIRecordIds\": [\n        99\n      ]\n    }\n  }\n}"
      }
     },
     "response": []
    },
    {
     "name": "Update record — composite-key 422 (Wave 1 demo)",
     "request": {
      "method": "PUT",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records/CUST-001",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records",
        "CUST-001"
       ]
      },
      "description": "**Demonstrates Wave 1 composite-key immutability.** Sending a `KeyPosition` property on PUT returns 422 with a per-field issue. Identity is the row's address, not its data; integrators must DELETE + INSERT or change the natural key in the source system.\n\nRound-trip `GET → modify → PUT` patterns must strip any `KeyPosition` field before submitting — `/schema` flags those fields with `keyPosition: 1/2/3`.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"CustomerNumber\": \"CUST-CHANGED\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Create record — stale expectedLayoutVersion 422 (Wave 1 demo)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/records",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "records"
       ]
      },
      "description": "**Demonstrates Wave 1 optimistic concurrency.** Sending an `expectedLayoutVersion` that doesn't match the form's current version returns 422 + `currentLayoutVersion` echoed back. Re-fetch /schema and retry with the fresh version.\n\n**Two body shapes accepted:**\n- Wrapper (recommended): `{ \"expectedLayoutVersion\": N, \"payload\": { … } }`\n- Inline: `{ \"expectedLayoutVersion\": N, \"Field1\": \"...\" }`\n\nField omitted ⇒ no concurrency check (default).",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 9223372036854775807,\n  \"payload\": {\n    \"CustomerNumber\": \"CUST-STALE-001\",\n    \"OrgName\": \"Stale layout test\"\n  }\n}"
      }
     },
     "response": []
    },
    {
     "name": "PR #325 — View-bound form list inherits view's sortColumns",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CarrierLockedEquipmentForm/records/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CarrierLockedEquipmentForm",
        "records",
        "list"
       ]
      },
      "description": "PR #325 — when a form is bound to a view (form.EntityInstanceViewId set), the form's records/list endpoint auto-merges the view's saved sortColumns[] into the proc-target wire shape via FormRecordsHelper.ComposeListSortJson. Caller-supplied sort still wins (precedence: explicit body > shortcut > view-baseline). Empty/missing → server-default Key1/Key2/Key3 order. The Carrier Locked form inherits its view's saved sort automatically."
     },
     "response": []
    },
    {
     "name": "Create a Form (authoring — LayoutJson over HTTP)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"VisitStatusFormApiDemo\",\n  \"description\": \"Visit lifecycle editor on WorkOrderVisit - authored over HTTP (modeled on the shipped VisitStatusForm).\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"expectedLayoutVersion\": 0,\n  \"layoutJson\": \"{\\\"version\\\": 1, \\\"mode\\\": \\\"single\\\", \\\"grid\\\": {\\\"columns\\\": 24}, \\\"compatibility\\\": [\\\"web\\\", \\\"mobile\\\"], \\\"allowNew\\\": false, \\\"allowEdit\\\": true, \\\"allowDelete\\\": false, \\\"sections\\\": [{\\\"internalName\\\": \\\"visit\\\", \\\"title\\\": \\\"Visit\\\", \\\"order\\\": 0, \\\"fields\\\": [{\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"DispatchNumber\\\", \\\"c\\\": 0, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1, \\\"required\\\": true, \\\"helpText\\\": \\\"Dispatch / work order number - key field 1 - locked after create.\\\"}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"EmployeeIdRef\\\", \\\"c\\\": 8, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1, \\\"required\\\": true, \\\"helpText\\\": \\\"Assigned technician - key field 2 - locked after create.\\\"}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"VisitCounter\\\", \\\"c\\\": 16, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1, \\\"required\\\": true, \\\"helpText\\\": \\\"Visit sequence number - key field 3 - locked after create.\\\"}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"WorkOrderNumber\\\", \\\"c\\\": 0, \\\"r\\\": 1, \\\"w\\\": 12, \\\"h\\\": 1, \\\"helpText\\\": \\\"Parent work order reference.\\\"}]}, {\\\"internalName\\\": \\\"status\\\", \\\"title\\\": \\\"Status\\\", \\\"order\\\": 1, \\\"fields\\\": [{\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"VisitStatus\\\", \\\"c\\\": 0, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1, \\\"renderMode\\\": \\\"dropdown\\\", \\\"required\\\": true, \\\"helpText\\\": \\\"Choice property - its own PropertyOption set drives the dropdown (Scheduled / Traveling / Arrived / Working / Completed / OnHold / Cancelled).\\\"}]}, {\\\"internalName\\\": \\\"timeline\\\", \\\"title\\\": \\\"Timeline\\\", \\\"order\\\": 2, \\\"fields\\\": [{\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"TravelingDateTime\\\", \\\"c\\\": 0, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"ArrivalTime\\\", \\\"c\\\": 8, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"WorkStartDateTime\\\", \\\"c\\\": 16, \\\"r\\\": 0, \\\"w\\\": 8, \\\"h\\\": 1}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"WorkStopDateTime\\\", \\\"c\\\": 0, \\\"r\\\": 1, \\\"w\\\": 8, \\\"h\\\": 1}, {\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"DepartedTime\\\", \\\"c\\\": 8, \\\"r\\\": 1, \\\"w\\\": 8, \\\"h\\\": 1}]}, {\\\"internalName\\\": \\\"notes\\\", \\\"title\\\": \\\"Notes\\\", \\\"order\\\": 3, \\\"fields\\\": [{\\\"entity\\\": \\\"WorkOrderVisit\\\", \\\"property\\\": \\\"VisitNotes\\\", \\\"c\\\": 0, \\\"r\\\": 0, \\\"w\\\": 24, \\\"h\\\": 2, \\\"controlVariant\\\": \\\"textarea\\\", \\\"helpText\\\": \\\"Free-text visit notes.\\\"}]}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/WorkOrderVisit/forms",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "WorkOrderVisit",
        "forms"
       ]
      },
      "description": "**Author a form over HTTP — `POST /api/v1/entities/{idOrName}/forms`** (role `DataEdit`; `DataAdmin` covers it).\n\nThis is the *authoring* surface: it creates the form **definition** (its `LayoutJson`), not a data row. The companion non-scoped route is `POST /api/v1/forms` — identical, except the entity comes from the body's `entityId` instead of the URL. Use the entity-scoped route here so you address the entity by friendly name (`WorkOrderVisit`) and never touch a numeric id.\n\nThis example is modeled on the shipped **VisitStatusForm** on `WorkOrderVisit` — a view-less (binds directly to the entity), keyed single-record editor with a **Choice property rendered as a dropdown** plus a timeline of DateTime fields. It backs the messaging start/working/done actions.\n\n**Lifecycle:** discover (`/api/v1/entities/WorkOrderVisit/properties/list`) → build the `LayoutJson` → validate (`POST /api/v1/entities/WorkOrderVisit/forms/validate`, never writes, returns `{ valid, issues[] }`) → save (this request) → use it through the records surface.\n\n**The `internalName` here is `VisitStatusFormApiDemo`** so it won't collide with the seeded `VisitStatusForm` (a duplicate name per entity → `409`). Swap it (and the entity name in the URL + every field's `entity`) to author your own form.\n\n---\n\n### The body envelope (everything outside `layoutJson`)\n\n| Field | Type | Required | Meaning |\n|---|---|---|---|\n| `entityFormLayoutId` | long | no (0 / omit = create) | The layout to update; a non-zero id updates that layout. Omitted here = create. |\n| `entityId` | long | only on `/api/v1/forms` | The primary entity. **Ignored on this entity-scoped route** (the URL wins). |\n| `internalName` | string | **yes** | Unique-per-entity form key — how you address the form everywhere else. |\n| `description` | string | no | Free text. |\n| `entityInstanceViewId` | long | no | Bind to a **view** (filter-as-lock derives from its eq-rules). Null here. |\n| `reportDefinitionId` | long | no | Bind to a **report** (cross-entity chain). Null here. |\n| `isShared` | bool | no | Visible to all tenant users. |\n| `isApiVisible` | bool | no | Exposed to external integrators — set `true` for API-driven forms. |\n| `layoutJson` | string | **yes** | The form definition, a JSON **string** (not an inline object). Schema below. |\n| `expectedLayoutVersion` | long | no | Optimistic concurrency on update; `0` / omit skips the check. |\n\n**Binding rule (XOR):** set **at most one** of `entityInstanceViewId` / `reportDefinitionId`. Both null = bind-directly-to-entity (this form). Both set → `409`.\n\n**Pre-save validation is automatic** — the server runs `FormLayoutValidator` before persisting; a hard error returns `400` with the full `issues[]` and never writes a ghost row.\n\nSee `docs/public/Integrators/Forms-API.md` for the full authoring reference and the `VisitStatusForm` walkthrough. The full `LayoutJson` skeleton + every key's notes live on this folder's description."
     },
     "response": [
      {
       "name": "200 OK — saved",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"entityFormLayoutId\": 57,\n  \"layoutVersion\": 1\n}"
      },
      {
       "name": "400 Bad Request — layoutJson failed validation",
       "status": "Bad Request",
       "code": 400,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"layoutJson has 1 validation error(s). First: sections[0].fields[1].property: Unknown property 'EmployeeIdRf' on entity 'WorkOrderVisit'. (suggestion: 'EmployeeIdRef')\",\n  \"issues\": [\n    {\n      \"path\": \"sections[0].fields[1].property\",\n      \"severity\": \"error\",\n      \"message\": \"Unknown property 'EmployeeIdRf' on entity 'WorkOrderVisit'.\",\n      \"suggestion\": \"EmployeeIdRef\"\n    }\n  ]\n}"
      },
      {
       "name": "409 Conflict — duplicate internalName for the entity",
       "status": "Conflict",
       "code": 409,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"A form layout named 'VisitStatusFormApiDemo' already exists for this entity.\"\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "Forms API — Shortcuts",
   "description": "Phase 3.2c — saved per-form filter state. An EntityFormShortcut captures a FilterGroup JSON scoped to one form, IsShared/IsApiVisible across three scopes (personal/shared/API). The apply-shortcut endpoint merges the shortcut's eq-rules with the form's baseline filter-as-lock and returns the combined lockedFields map.\n\n{shortcutIdOrName} accepts numeric id OR friendly InternalName (case-insensitive, scoped to the form).\n\nFilterJson uses the same FilterGroup shape views and reports use ({ logic, rules[], groups[] }) — one canonical filter shape across every platform surface.",
   "item": [
    {
     "name": "List shortcuts on a form",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CTOnlyServiceLocationForm/shortcuts/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CTOnlyServiceLocationForm",
        "shortcuts",
        "list"
       ]
      },
      "description": "Returns caller's own + shared shortcuts for a form. Each row carries FilterJson so clients can introspect before applying. Seeded demo shortcut on CTOnlyServiceLocationForm: HartfordOnly (City=Hartford)."
     },
     "response": []
    },
    {
     "name": "Get shortcut by friendly name",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CTOnlyServiceLocationForm/shortcuts/HartfordOnly",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CTOnlyServiceLocationForm",
        "shortcuts",
        "HartfordOnly"
       ]
      },
      "description": "Resolve a single shortcut by numeric EntityFormShortcutId OR friendly InternalName. Scope is per-form; resolving a shortcut through the wrong form returns 404."
     },
     "response": []
    },
    {
     "name": "Save shortcut (create)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entityFormShortcutId\": 0,\n  \"internalName\": \"CustomersWithEastern\",\n  \"description\": \"Any customer with 'Eastern' in the org name.\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"filterJson\": \"{\\\"logic\\\":\\\"AND\\\",\\\"rules\\\":[{\\\"entity\\\":\\\"Customer\\\",\\\"property\\\":\\\"OrgName\\\",\\\"op\\\":\\\"contains\\\",\\\"value\\\":\\\"Eastern\\\"}],\\\"groups\\\":null}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/shortcuts",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "shortcuts"
       ]
      },
      "description": "Create a new shortcut. entityFormShortcutId=0 signals create; existing id updates in place. FilterJson must parse as a FilterGroup — malformed JSON fails fast with 400 (no garbage persisted). Private shortcuts cannot be API-visible; the server forces IsApiVisible=false when IsShared=false regardless of what you send."
     },
     "response": []
    },
    {
     "name": "Delete shortcut",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CustomerBasicForm/shortcuts/CustomersWithEastern",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CustomerBasicForm",
        "shortcuts",
        "CustomersWithEastern"
       ]
      },
      "description": "Soft-delete by numeric id OR friendly name. System shortcuts (IsSystem=true) return 409. Deleted shortcuts remain in hist.EntityFormShortcut."
     },
     "response": []
    },
    {
     "name": "Apply shortcut — list records through the saved filter",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"page\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/forms/CTOnlyServiceLocationForm/shortcuts/HartfordOnly/records/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "forms",
        "CTOnlyServiceLocationForm",
        "shortcuts",
        "HartfordOnly",
        "records",
        "list"
       ]
      },
      "description": "Dedicated apply endpoint — the shortcut's FilterGroup AND-merges with the form's baseline filter-as-lock. For HartfordOnly on CTOnlyServiceLocationForm the response's lockedFields carries { State: CT, City: Hartford } — BOTH the form's baseline and the shortcut's eq-rule auto-applied. Non-eq rules (contains / gt / lt / between / etc.) become additional list-scope filters. Integrators bookmark this URL as a stable entry point for 'show me Hartford sites' — no query string, no cache-leak risk, friendly-name resolvable."
     },
     "response": [
      {
       "name": "200 OK — Hartford rows, merged lockedFields",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"records\": [\n    { \"eiRecordId\": 101, \"externalKey\": \"CUST-001 / LOC-001\", \"data\": { \"CustomerNumber\": \"CUST-001\", \"LocationNumber\": \"LOC-001\", \"City\": \"Hartford\", \"State\": \"CT\" }, \"modifiedDate\": \"2026-04-21T10:15:00Z\" }\n  ],\n  \"page\": 1,\n  \"pageSize\": 50,\n  \"lockedFields\": {\n    \"State\": \"CT\",\n    \"City\":  \"Hartford\"\n  }\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "Menu Manager",
   "description": "Tenant-scoped navigation items — pinned forms/views/reports, external links, storage shortcuts. Three-scope merge (system / tenant-shared / personal pin). Reads: any auth; writes: AdminSettings. See Integrators/TenantAdmin-API.md § Menu Manager.",
   "item": [
    {
     "name": "List Menu Items",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"menuItemType\": \"Form\",\n  \"channelFlag\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Flat list ordered system -> shared -> mine. All filters optional: menuItemType(Id), channelFlag, homeArea(Id), languageCode(LanguageRegionId). channelFlag is a surface bitmask (dbo.Channel.ChannelId, powers of two): 1 Web, 2 Mobile, 4 Tablet, 8 Print, 16 RBM, 32 RCS, 64 AMB, 128 SMS, 256 Email, 512 WhatsApp. Pass a bit to get only items on that surface (the Portal uses 1, mobile 2, the messaging MENU command the inbound channel's bit)."
     }
    },
    {
     "name": "Get Menu Tree",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/tree",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "tree"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same filters as /list but pre-nested as top-level roots with children[]. Use for rendering a navigation tree."
     }
    },
    {
     "name": "Get Menu Targets (AdminSettings)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/targets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "targets"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Enumerate every linkable target — Forms, Views, Reports, StorageShortcuts — so you can pick what a new menu item points at without memorizing InternalName strings."
     }
    },
    {
     "name": "Get Menu Item (by InternalName)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/TenantNavListrak",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "TenantNavListrak"
       ]
      },
      "description": "Detail for one item by InternalName or numeric id. Returns the full row plus per-language displays[]."
     }
    },
    {
     "name": "Create Menu Item (pinned form)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"MyPinnedCustomerForm\",\n  \"menuItemType\": \"Form\",\n  \"targetId\": 10,\n  \"iconKey\": \"bi-person\",\n  \"displayOrder\": 50,\n  \"parentInternalName\": \"TenantNav\",\n  \"displays\": [\n    { \"languageCode\": \"en\", \"name\": \"Customer Form\", \"description\": \"Pinned shortcut\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a new menu item. Defaults: channelFlag=3 (Web+Mobile), isVisibleInMenu=true, isApiVisible=false, roleFlag=0, isShared=true for tenant-wide / false for personal. Requires AdminSettings."
     }
    },
    {
     "name": "Create Menu Item (API-only directory entry)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"FormsCatalogApi\",\n  \"menuItemType\": \"ExternalUrl\",\n  \"externalUrl\": \"https://api.example.com/forms\",\n  \"channelFlag\": 0,\n  \"isApiVisible\": true,\n  \"displays\": [\n    { \"languageCode\": \"en\", \"name\": \"Forms Catalog\", \"description\": \"API-only directory — invisible on human surfaces.\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "channelFlag=0 + isApiVisible=true = API-only directory entry. Invisible on Web/Mobile/etc.; surfaces only to integrator API-key callers."
     }
    },
    {
     "name": "Create Menu Item (Workflow pinned to WhatsApp + Mobile)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"StartDailyInspection\",\n  \"menuItemType\": \"Workflow\",\n  \"workflowInternalName\": \"DailyInspection\",\n  \"iconKey\": \"bi-diagram-3\",\n  \"channelFlag\": 514,\n  \"displays\": [\n    { \"languageCode\": \"en\", \"name\": \"Daily Inspection\", \"description\": \"Start the inspection workflow\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "MenuItemType=8 Workflow (targetId = WorkflowId, or workflowInternalName). channelFlag 514 = Mobile(2) + WhatsApp(512): the item shows in the mobile app AND when a contact texts MENU on WhatsApp, where selecting it starts the workflow conversation. Channel bits (dbo.Channel.ChannelId, powers of two): 1 Web, 2 Mobile, 4 Tablet, 8 Print, 16 RBM, 32 RCS, 64 AMB, 128 SMS, 256 Email, 512 WhatsApp. Combine bits to span surfaces."
     }
    },
    {
     "name": "Update Menu Item (partial)",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/MyPinnedCustomerForm",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "MyPinnedCustomerForm"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"iconKey\": \"bi-bookmark\",\n  \"displayOrder\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Partial merge — send only the fields you want to change. All other fields preserved. Requires AdminSettings. Rejects updates to IsSystem=true rows."
     }
    },
    {
     "name": "Reorder Menu Item",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/MyPinnedCustomerForm/reorder",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "MyPinnedCustomerForm",
        "reorder"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"displayOrder\": 20,\n  \"parentInternalName\": \"TenantNav\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Change displayOrder and optionally reparent in one call. Two-level nesting cap enforced."
     }
    },
    {
     "name": "Save Display (Spanish)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/MyPinnedCustomerForm/display",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "MyPinnedCustomerForm",
        "display"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"languageCode\": \"es\",\n  \"name\": \"Formulario Cliente\",\n  \"description\": \"Atajo fijado\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Upsert one language's Name + Description. Call once per language you want to support. Tenant default-language fallback is automatic."
     }
    },
    {
     "name": "Delete Menu Item",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/menu-items/MyPinnedCustomerForm",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "menu-items",
        "MyPinnedCustomerForm"
       ]
      },
      "description": "Soft delete (ActiveEnd = now). System-seeded rows (IsSystem=true) return 422. Requires AdminSettings."
     }
    }
   ]
  },
  {
   "name": "Surfaces — Pillar 8",
   "description": "Tenant-authored multi-canvas pages (Portal + Mobile + RCS) — one LayoutJson, three renderers. The eighth pillar.\n\nCRUD a Surface, validate before save (optimistic concurrency via expectedLayoutVersion), list all surfaces visible to the caller.\n\nRequires DataView for reads, BuilderAdmin for writes. Acme demo set: AcmeMyJobs, AcmeJobDetail, AcmeFieldHome, AcmeDispatcherDashboard, AcmeShortcutsHub, AcmeFieldVisit, AcmeVarianceDashboard, AcmeEquipmentBoard, AcmeCarrierHub, AcmeKindShowcase (all IsSystem=true; tenants clone-then-edit).\n\nSee docs/public/Integrators/Surfaces-Guide.md for the partner-facing guide + the cookbook recipes.",
   "item": [
    {
     "name": "List Surfaces",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "list"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Returns all surfaces visible to the caller — own + shared (IsShared=true) + system (IsSystem=true). Filter client-side by InternalName prefix to find a family. Each row carries surfaceLayoutId, internalName, isShared, isSystem, isApiVisible, layoutVersion, name, description, ownerTenantUserId, modifiedDate. DataView role required."
     }
    },
    {
     "name": "Get Surface — by InternalName",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/AcmeMyJobs",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "AcmeMyJobs"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Detail of the AcmeMyJobs system-seeded demo. Path slot accepts either InternalName (string) or numeric SurfaceLayoutId. Response includes the full layoutJson (string), layoutVersion (int for optimistic concurrency), audit columns, and the per-language Name + Description."
     }
    },
    {
     "name": "Get Surface — by numeric id",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/1",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "1"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Same endpoint, numeric path. Discover ids from the /list response — every row carries surfaceLayoutId."
     }
    },
    {
     "name": "Create Surface — minimal example",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"MyFirstSurface\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"My First Surface\",\n  \"description\": \"Authored via Postman\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"MyFirstSurface\\\",\\\"displayName\\\":{\\\"default\\\":\\\"My First Surface\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"currentUser\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Hello, Surfaces!\\\"},\\\"config\\\":{\\\"tagline\\\":\\\"Pillar 8\\\",\\\"subtitle\\\":\\\"One author, every canvas.\\\",\\\"overlay\\\":\\\"dark\\\"}},{\\\"key\\\":\\\"jobs\\\",\\\"kind\\\":\\\"tiles\\\",\\\"title\\\":{\\\"default\\\":\\\"My visits\\\"},\\\"bindingSlot\\\":\\\"PendingVisits\\\",\\\"config\\\":{\\\"tileSize\\\":\\\"wide\\\",\\\"titleField\\\":\\\"DispatchNumber\\\",\\\"subtitleField\\\":\\\"ServiceType\\\",\\\"drillRouteTemplate\\\":\\\"/s/AcmeJobDetail?Key1={key1}\\\"}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Create a new Surface. layoutJson is sent as a JSON-encoded string (escape the inner quotes). The validator runs server-side BEFORE persistence — invalid payloads return 422 with the issue list in the response body. After save, the new surfaceLayoutId is in the response; bookmark the InternalName for future updates. Requires BuilderAdmin."
     }
    },
    {
     "name": "Update Surface — optimistic concurrency",
     "request": {
      "method": "PUT",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/MyFirstSurface",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "MyFirstSurface"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"expectedLayoutVersion\": 1,\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"My First Surface (Updated)\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"MyFirstSurface\\\",\\\"displayName\\\":{\\\"default\\\":\\\"My First Surface\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"currentUser\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Hello again!\\\"}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Update a surface. expectedLayoutVersion must match the server's stored value or you'll get 409 Conflict — re-fetch via /surfaces/{name}, apply your changes on top of the fresh row, retry. The Save proc increments layoutVersion on every successful UPDATE so concurrent authors don't trample each other.\n\nSystem-seeded rows (isSystem=true) return 409 on update — tenants clone via Create with a new InternalName, then edit the clone."
     }
    },
    {
     "name": "Validate Surface — dry-run (recommended pre-save)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/MyFirstSurface/validate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "MyFirstSurface",
        "validate"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"MyFirstSurface\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Test\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"currentUser\\\"},\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Hi\\\"}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Dry-run the validator without saving. Returns { valid: bool, issues: [{ path, severity, message, suggestion }, ...] }. Same rules the Save endpoint runs server-side. Use this from CI to catch schema drift before deploying authored surfaces. Errors block save; warnings flag suspicious shapes (e.g. mobile-only kinds on a portal-only surface)."
     }
    },
    {
     "name": "Delete Surface",
     "request": {
      "method": "DELETE",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/MyFirstSurface",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "MyFirstSurface"
       ]
      },
      "description": "Hard delete (FK 547 returns 409 Conflict — clean up referencing rows first). System-seeded rows (isSystem=true) return 409 — tenants can't delete the demo Acme set. BuilderAdmin required."
     }
    },
    {
     "name": "Hydrate Surface (one-shot)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces/AcmeMyJobs/hydrate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces",
        "AcmeMyJobs",
        "hydrate"
       ]
      },
      "description": "One-shot batcher — returns the Surface's LayoutJson AND its fully-hydrated ContextDefinition bundle in a single round-trip. Mobile + Portal /s/{Name} routes use this to avoid two HTTP calls on cold load. DataView role."
     }
    },
    {
     "name": "Example — author a Field-Tech Home (currentUser anchor)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoFieldHome\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Field Home (demo)\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoFieldHome\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Field Home\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"currentUser\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Welcome back\\\"},\\\"config\\\":{\\\"tagline\\\":\\\"Field tech home\\\",\\\"subtitle\\\":\\\"Your active dispatches at a glance.\\\",\\\"overlay\\\":\\\"dark\\\"}},{\\\"key\\\":\\\"metrics\\\",\\\"kind\\\":\\\"metrics-strip\\\",\\\"config\\\":{\\\"metrics\\\":[{\\\"label\\\":\\\"Active\\\",\\\"value\\\":\\\"5\\\",\\\"trend\\\":\\\"+2\\\",\\\"trendColor\\\":\\\"success\\\"},{\\\"label\\\":\\\"Completed today\\\",\\\"value\\\":\\\"7\\\"}]}},{\\\"key\\\":\\\"jobs\\\",\\\"kind\\\":\\\"tiles\\\",\\\"title\\\":{\\\"default\\\":\\\"My visits\\\"},\\\"bindingSlot\\\":\\\"PendingVisits\\\",\\\"config\\\":{\\\"tileSize\\\":\\\"wide\\\",\\\"titleField\\\":\\\"DispatchNumber\\\",\\\"subtitleField\\\":\\\"ServiceType\\\",\\\"captionField\\\":\\\"DatePromised\\\",\\\"drillRouteTemplate\\\":\\\"/s/AcmeJobDetail?Key1={key1}\\\"}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Author a field-tech home surface that mirrors UI.Mobile/Pages/Jobs.razor as a declarative Surface. anchorSource=currentUser means no URL params — the Context Engine's currentTenantUser anchor pulls THIS user's active dispatches via the polymorphic identity bridge. Same authored payload renders on Portal (/s/DemoFieldHome) and Mobile (/mobile/s/DemoFieldHome). Each tile drills to AcmeJobDetail with Key1 pre-filled from the visit's DispatchNumber."
     }
    },
    {
     "name": "Example — embed a saved Report (Variance Dashboard pattern)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoVariance\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Variance demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoVariance\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Variance Dashboard\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"none\\\"},\\\"compatibility\\\":[\\\"portal\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Variance\\\"},\\\"config\\\":{\\\"tagline\\\":\\\"Equipment drift over time\\\",\\\"overlay\\\":\\\"dark\\\"}},{\\\"key\\\":\\\"monthly\\\",\\\"kind\\\":\\\"embed-report\\\",\\\"title\\\":{\\\"default\\\":\\\"Monthly variance\\\"},\\\"config\\\":{\\\"reportIdOrName\\\":\\\"Equipment Monthly Variance\\\",\\\"maxRows\\\":24}},{\\\"key\\\":\\\"trend\\\",\\\"kind\\\":\\\"chart\\\",\\\"title\\\":{\\\"default\\\":\\\"7-day trend\\\"},\\\"config\\\":{\\\"chartType\\\":\\\"line\\\",\\\"valueColor\\\":\\\"success\\\",\\\"data\\\":[{\\\"label\\\":\\\"Mon\\\",\\\"value\\\":12},{\\\"label\\\":\\\"Tue\\\",\\\"value\\\":18},{\\\"label\\\":\\\"Wed\\\",\\\"value\\\":15},{\\\"label\\\":\\\"Thu\\\",\\\"value\\\":22},{\\\"label\\\":\\\"Fri\\\",\\\"value\\\":19},{\\\"label\\\":\\\"Sat\\\",\\\"value\\\":7},{\\\"label\\\":\\\"Sun\\\",\\\"value\\\":4}]}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates embed-report — inline a saved Report's results (all joins / filters / aggregates intact). Pair with chart + gauge for the variance dashboard shape. Same pattern in the AcmeVarianceDashboard system demo. The report's columns + rows arrive in a generic tabular shape; embed-report renders them with the table chrome."
     }
    },
    {
     "name": "Example — embed an EntityInstanceView (per-entity scope)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoEquipBoard\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Equipment board demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoEquipBoard\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Equipment\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"none\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Equipment\\\"}},{\\\"key\\\":\\\"all\\\",\\\"kind\\\":\\\"embed-view\\\",\\\"title\\\":{\\\"default\\\":\\\"All equipment\\\"},\\\"config\\\":{\\\"entityIdOrName\\\":\\\"Equipment\\\",\\\"viewIdOrName\\\":\\\"All Equipment\\\",\\\"maxRows\\\":100}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates embed-view — surface a saved EntityInstanceView inline. NOTE: both entityIdOrName AND viewIdOrName are required (the Views API is scoped per-entity at /api/v1/entities/{idOrName}/views/{viewIdOrName}/named). Both ids accept friendly names or numeric ids."
     }
    },
    {
     "name": "Example — chart + gauge + metrics-strip dashboard",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoKpiDashboard\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"KPI dashboard demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoKpiDashboard\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Operations dashboard\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"none\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Operations\\\"}},{\\\"key\\\":\\\"strip\\\",\\\"kind\\\":\\\"metrics-strip\\\",\\\"config\\\":{\\\"metrics\\\":[{\\\"label\\\":\\\"Open\\\",\\\"value\\\":\\\"47\\\",\\\"trend\\\":\\\"+8\\\",\\\"trendColor\\\":\\\"warning\\\"},{\\\"label\\\":\\\"Completed\\\",\\\"value\\\":\\\"182\\\",\\\"trend\\\":\\\"+12\\\",\\\"trendColor\\\":\\\"success\\\"}]}},{\\\"key\\\":\\\"gauge\\\",\\\"kind\\\":\\\"gauge\\\",\\\"title\\\":{\\\"default\\\":\\\"Avg cycle\\\"},\\\"config\\\":{\\\"value\\\":\\\"4.2h\\\",\\\"valueColor\\\":\\\"success\\\",\\\"trend\\\":\\\"down 8%\\\",\\\"trendColor\\\":\\\"success\\\",\\\"icon\\\":\\\"bi-stopwatch\\\"}},{\\\"key\\\":\\\"chart\\\",\\\"kind\\\":\\\"chart\\\",\\\"title\\\":{\\\"default\\\":\\\"Last 7 days\\\"},\\\"config\\\":{\\\"chartType\\\":\\\"line\\\",\\\"valueColor\\\":\\\"success\\\",\\\"height\\\":220,\\\"data\\\":[{\\\"label\\\":\\\"Mon\\\",\\\"value\\\":12},{\\\"label\\\":\\\"Tue\\\",\\\"value\\\":18},{\\\"label\\\":\\\"Wed\\\",\\\"value\\\":15},{\\\"label\\\":\\\"Thu\\\",\\\"value\\\":22}]}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Operations dashboard mix: hero + metrics-strip + gauge + chart. anchorSource=none means no URL params; bundle resolves against tenant-wide queries. Same authored payload renders on Portal AND Mobile via the universal SurfaceRunner."
     }
    },
    {
     "name": "Example — kanban dispatch board",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoKanbanBoard\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Dispatch board demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoKanbanBoard\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Dispatch board\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"none\\\"},\\\"compatibility\\\":[\\\"portal\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"board\\\",\\\"kind\\\":\\\"kanban\\\",\\\"title\\\":{\\\"default\\\":\\\"Dispatch\\\"},\\\"bindingSlot\\\":\\\"PendingVisits\\\",\\\"config\\\":{\\\"groupByField\\\":\\\"Status\\\",\\\"columns\\\":[\\\"Pending\\\",\\\"In Progress\\\",\\\"Completed\\\",\\\"Cancelled\\\"],\\\"titleField\\\":\\\"DispatchNumber\\\",\\\"subtitleField\\\":\\\"ServiceType\\\",\\\"drillRouteTemplate\\\":\\\"/s/AcmeJobDetail?Key1={key1}\\\"}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates kanban — column board grouping a bundle array slot by a field. compatibility=[portal] only because the wide column layout doesn't fit mobile; mobile users see a placeholder. columns is authored explicitly (NOT auto-enumerated). drillRouteTemplate gives every card a drill-to-detail tap."
     }
    },
    {
     "name": "Example — embed-surface composition (depth + cycle protected)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoNestedHome\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Nested home demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoNestedHome\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Field home\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_TechWorklist\\\",\\\"anchorSource\\\":\\\"currentUser\\\"},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"hero\\\",\\\"kind\\\":\\\"hero-banner\\\",\\\"title\\\":{\\\"default\\\":\\\"Welcome\\\"}},{\\\"key\\\":\\\"shortcuts\\\",\\\"kind\\\":\\\"embed-surface\\\",\\\"title\\\":{\\\"default\\\":\\\"Quick access\\\"},\\\"config\\\":{\\\"surfaceInternalName\\\":\\\"AcmeShortcutsHub\\\",\\\"passAnchorIdentity\\\":false}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates embed-surface — compose another Surface inline. Depth cap is 5 hops; cycle detection runs at save (self-reference) AND hydrate (multi-hop). passAnchorIdentity=true forwards the parent's resolved anchor to the embedded Surface (useful when the embedded Surface uses a route anchor on the same key)."
     }
    },
    {
     "name": "Example — channel-aware capture (signature + camera + geo-checkin)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoFieldVisitCapture\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Field visit capture\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoFieldVisitCapture\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Visit capture\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_DispatchContext\\\",\\\"anchorSource\\\":\\\"route\\\",\\\"anchorRouteMapping\\\":{\\\"{Key1}\\\":\\\"WorkOrderNumber\\\"}},\\\"compatibility\\\":[\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"arrival\\\",\\\"kind\\\":\\\"geo-checkin\\\",\\\"title\\\":{\\\"default\\\":\\\"Confirm arrival\\\"},\\\"config\\\":{\\\"label\\\":\\\"I am on site\\\",\\\"thresholdMeters\\\":250}},{\\\"key\\\":\\\"photo\\\",\\\"kind\\\":\\\"camera\\\",\\\"title\\\":{\\\"default\\\":\\\"Equipment photo\\\"},\\\"config\\\":{\\\"label\\\":\\\"Take photo\\\",\\\"maxBytes\\\":25000000}},{\\\"key\\\":\\\"sig\\\",\\\"kind\\\":\\\"signature\\\",\\\"title\\\":{\\\"default\\\":\\\"Customer sign-off\\\"},\\\"config\\\":{\\\"label\\\":\\\"Capture signature\\\",\\\"maxBytes\\\":10000000}}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates the channel-aware capture kinds — geo-checkin + camera + signature. Mobile-only via compatibility=[mobile]; Portal renders friendly placeholders for these on a portal-compat surface (they need ISurfaceAssetUpload + ISurfaceLocationCapture providers, only registered on Mobile). thresholdMeters guards the geo-checkin so the user must be physically near the expected location."
     }
    },
    {
     "name": "Example — search anchor (debounced picker)",
     "request": {
      "method": "POST",
      "url": {
       "raw": "{{baseUrl}}/api/v1/surfaces",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "surfaces"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"DemoCustomerLookup\",\n  \"isShared\": true,\n  \"isApiVisible\": true,\n  \"name\": \"Customer lookup demo\",\n  \"layoutJson\": \"{\\\"version\\\":1,\\\"internalName\\\":\\\"DemoCustomerLookup\\\",\\\"displayName\\\":{\\\"default\\\":\\\"Find a customer\\\"},\\\"context\\\":{\\\"definitionInternalName\\\":\\\"Acme_CustomerOverview\\\",\\\"anchorSource\\\":\\\"search\\\",\\\"search\\\":{\\\"entityRole\\\":\\\"Customer\\\",\\\"viewInternalName\\\":\\\"All Customers\\\",\\\"searchFields\\\":[\\\"OrgName\\\",\\\"CustomerNumber\\\",\\\"DispatchPhone\\\"],\\\"displayFields\\\":[\\\"OrgName\\\",\\\"City\\\",\\\"State\\\"]}},\\\"compatibility\\\":[\\\"portal\\\",\\\"mobile\\\"],\\\"sections\\\":[{\\\"key\\\":\\\"customer\\\",\\\"kind\\\":\\\"card\\\",\\\"bindingSlot\\\":\\\"Customer\\\",\\\"fields\\\":[{\\\"field\\\":\\\"OrgName\\\",\\\"emphasis\\\":\\\"hero\\\"},{\\\"field\\\":\\\"CustomerNumber\\\"},{\\\"field\\\":\\\"DispatchPhone\\\"}]}]}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Demonstrates anchorSource=search. URL /s/DemoCustomerLookup renders a debounced search box first; the user picks a row; the row's keys become the anchor identity; the bundle hydrates; the rest of the sections render. searchFields drive the LIKE query; displayFields control the picker row layout. Optional viewInternalName scopes the search to a filtered subset (here: All Customers view)."
     }
    }
   ]
  },
  {
   "name": "EntitySync — Dream Wizard",
   "description": "The agent → Portal hand-off endpoints that drive the dream wizard. Three sequential calls:\n\n  1. POST /api/v1/assets — upload the file (multipart). Returns assetKey.\n  2. POST /api/v1/sync/wizard/probe — read headers + sample rows.\n  3. POST /api/v1/sync/profiles/from-import — save Profile + binding + field maps.\n\nThe on-prem agent's --auto-import CLI runs all three headless in one command. The Portal wizard at /ops/integrations/profiles/new-from-import drives the same endpoints from a 7-step UI (or one click via ?autoImport=1).\n\nRequires AdminIntegration + FileUpload roles. Acme tenant test key has both.",
   "item": [
    {
     "name": "1. Upload file — multipart, returns assetKey",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/assets",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "assets"
       ]
      },
      "body": {
       "mode": "formdata",
       "formdata": [
        {
         "key": "file",
         "type": "file",
         "src": ""
        },
        {
         "key": "sensitive",
         "value": "true",
         "type": "text"
        },
        {
         "key": "subPath",
         "value": "uploads",
         "type": "text"
        },
        {
         "key": "title",
         "value": "MyData",
         "type": "text"
        },
        {
         "key": "description",
         "value": "Uploaded via Postman dream wizard test",
         "type": "text"
        }
       ]
      },
      "description": "Step 1 of the dream wizard. Uploads a CSV/TSV/Excel/PDF file via multipart/form-data and returns { assetKey, storageFileName, binaryAssetId, overwritten }.\n\nAttach a file to the 'file' form field. Other fields are optional but recommended:\n  • sensitive=true   — routes to the tenant's default-sensitive storage source (the wizard convention)\n  • subPath=uploads  — physical sub-path under the storage source's root\n  • title            — display title on the asset row (defaults to filename)\n  • description      — free-form provenance text\n\nServer enforces multipart body limit: 5 GB (bumped from default 128 MB to support real-world dispatch / customer dumps). Streams via IStorageProvider so memory stays constant. Same call the agent's --push-file CLI verb makes, same call the Portal wizard's FmUploadDialog makes.\n\nAuth: Bearer with FileUpload role (AdminIntegration alone isn't enough)."
     }
    },
    {
     "name": "2. Probe — read headers + samples from an uploaded asset",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/wizard/probe",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "wizard",
        "probe"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assetKey\": \"00000000-0000-0000-0000-000000000000\",\n  \"tabName\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Read a previously-uploaded file's headers + first 100 sample rows. Smart sniff for delimiter (extension is a hint, not a hard rule — Customers.tsv with comma content probes as csv). Excel two-step: first call without tabName returns sheets list; second call with tabName returns headers + samples. Response: { format, tabs?, selectedTab?, headers, sampleRows, totalRowsParsed, truncated, error }. Used by the Portal wizard's Step 2 + the agent's --push-file/--auto-import."
     }
    },
    {
     "name": "3. Save profile from import (Crossover)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/from-import",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "from-import"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"CustomerImport-2026-04-28\",\n  \"displayName\": \"Customer sync from customers.csv\",\n  \"connectorType\": \"Csv\",\n  \"runMode\": \"OnPrem\",\n  \"syncIntervalSeconds\": 3600,\n  \"entityId\": 100013,\n  \"sourceEntityName\": \"customers\",\n  \"syncDirection\": \"Push\",\n  \"propagateDeletes\": false,\n  \"writeMode\": \"overwrite\",\n  \"fieldMaps\": [\n    { \"sourceFieldName\": \"cust_no\",   \"entityPropertyId\": 100083, \"keyPosition\": 1 },\n    { \"sourceFieldName\": \"cust_name\", \"entityPropertyId\": 100010 },\n    { \"sourceFieldName\": \"cust_phone\",\"entityPropertyId\": 100012 }\n  ],\n  \"originContext\": \"Postman test 2026-04-28\",\n  \"filterJson\": null,\n  \"assignToAgentId\": null,\n  \"_phase2a_comment\": \"For SqlServer connectorType: pass sqlConnectionString here AND sourceQuery in the binding-side block when calling /api/v1/sync/profiles/{id}/entities. File-based connectors leave both null.\",\n  \"sqlConnectionString\": null,\n  \"sourceQuery\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Composite create — Profile + ProfileEntity (binding) + N FieldMaps in sequence, plus optional auto-assign to a registered agent. Used by both the Data Studio CSV import wizard's 'Save as recurring sync' button AND the dream wizard's Step 7 save AND the agent's --auto-import CLI. Returns { entitySyncProfileId, entitySyncProfileEntityId, fieldMapsSaved, fieldMapErrors, assignedAgentProfileId, assignError, redirectUrl }."
     }
    },
    {
     "name": "Wipe all instances (testing only — destructive)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/entities/Customer/instances/wipe-all",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "entities",
        "Customer",
        "instances",
        "wipe-all"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{\n  \"confirm\": true,\n  \"confirmEntityName\": \"Customer\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Soft-delete every live instance for an entity — 'shape + reload' testing flows. Two-key destructive guard: BOTH 'confirm: true' AND 'confirmEntityName' matching the entity's exact InternalName are required. GitHub-repo-delete pattern. Returns { wiped, requested, notFound } on success, 409 if any row is referenced by a child instance (FK 547). Caps at 100K rows; larger entities use /instances/bulk-delete with explicit batches. Production sync flows use PropagateDeletes + WriteMode=overwrite instead."
     }
    },
    {
     "name": "EntitySync — list profiles",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "list"
       ]
      },
      "description": "List sync profiles in the tenant. Each profile defines a target entity + field mappings + agent assignments + run history. Profile = the recipe; Agent = the running instance that executes it."
     },
     "response": []
    },
    {
     "name": "EntitySync — list agents",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agents/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agents",
        "list"
       ]
      },
      "description": "List sync agents registered in the tenant. Each agent has a hardware-baked GUID serial# (set on first registration), an enabled flag (admin-toggled, three-layer trust gate), and assigned profiles. Long-running .NET tools (Tools/ServiceProofSync) check in periodically and pull the profiles they're assigned."
     },
     "response": []
    },
    {
     "name": "EntitySync — toggle profile enabled",
     "request": {
      "method": "PUT",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"enabled\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/1/enabled",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "1",
        "enabled"
       ]
      },
      "description": "Profile-level enable gate. Three-layer trust check (Agent + Assignment + Profile all must be true) means flipping one false stops the sync immediately. Replace `1` with the profileId."
     },
     "response": []
    },
    {
     "name": "EntitySync — save profile (create OR update, Phase 2A field shape)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncProfileId\": null,\n  \"internalName\": \"EscEmployeeSync\",\n  \"displayName\": \"ESC Employee push\",\n  \"description\": \"Pushes ESC HR Employee rows up to platform Customer entity\",\n  \"connectorType\": \"SqlServer\",\n  \"runMode\": \"OnPrem\",\n  \"syncIntervalSeconds\": 300,\n  \"defaultConflictPolicy\": \"RemoteWins\",\n  \"defaultMergeTieBreaker\": \"Manual\",\n  \"isEnabled\": true,\n  \"notes\": \"Created via Postman 2026-05-08\",\n  \"_phase2a_comment\": \"sqlConnectionString is required when connectorType=SqlServer. Plaintext at rest in the platform DB; AdminIntegration-gated reads. The on-prem agent picks it up via mothership and overlays LocalStoreConfig.SqlConnection in memory only - never writes to disk.\",\n  \"sqlConnectionString\": \"Server=10.0.1.20\\\\ESC;Database=SERVICE;User Id=sa;Password=YOUR_PW;TrustServerCertificate=True;\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles"
       ]
      },
      "description": "Create OR update a sync profile. Pass `entitySyncProfileId: null` (or omit) to create; pass an existing id to update.\n\n**Phase 2A:** the `sqlConnectionString` field is required when `connectorType=SqlServer`. For file-based connectors (`Csv` / `Tsv` / `Excel` / `AcmeJson`), set it to `null` or omit the field entirely.\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**Response 200 OK:** `{ entitySyncProfileId: number }`\n\n**Conflict 409:** `{ error: \"A profile with this InternalName already exists.\" }` — pick a unique InternalName.\n\n**Use the [from-import](#dream-wizard) composite endpoint** for the common Crossover flow (Profile + binding + field maps in one call). This standalone endpoint is for the \"edit existing profile settings\" path the Portal `ProfileEdit` Settings tab uses."
     },
     "response": []
    },
    {
     "name": "EntitySync — save profile binding (Phase 2A field shape)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncProfileEntityId\": null,\n  \"bindingType\": \"Entity\",\n  \"entityId\": 100012,\n  \"entityFormLayoutId\": null,\n  \"entityInstanceViewId\": null,\n  \"reportDefinitionId\": null,\n  \"sourceEntityName\": \"Employee\",\n  \"syncDirection\": \"Push\",\n  \"conflictPolicy\": null,\n  \"mergeTieBreaker\": null,\n  \"isEnabled\": true,\n  \"position\": 1,\n  \"parentEntityId\": null,\n  \"filterJson\": null,\n  \"writeMode\": \"overwrite\",\n  \"propagateDeletes\": false,\n  \"_phase2a_comment\": \"sourceQuery is the saved SELECT-with-[Hash] T-SQL when the parent profile's connectorType=SqlServer. Mothership delivers it to the agent which writes queries/{sourceEntityName}.sql. Schema contract: project natural-key columns + every data column + a [Hash] column from CONVERT(VARCHAR(40), HASHBYTES('SHA1', (SELECT ... FOR XML RAW)), 2) with a CORRELATED subquery filtering to the outer row's keys. NULL for non-SQL bindings.\",\n  \"sourceQuery\": \"SELECT e.EmpNo, e.FirstName, e.LastName, e.Email, CONVERT(VARCHAR(40), HASHBYTES('SHA1', (SELECT e2.EmpNo, e2.FirstName, e2.LastName, e2.Email FROM dbo.Employee e2 WHERE e2.EmpNo = e.EmpNo FOR XML RAW)), 2) AS [Hash] FROM dbo.Employee e\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/1/entities",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "1",
        "entities"
       ]
      },
      "description": "Create OR update a binding (entity / form / view / report) on an existing profile. Replace `1` in the URL with the profileId.\n\n**Phase 2A:** the `sourceQuery` field is required when the parent profile's `connectorType=SqlServer` AND `bindingType=Entity` (the only push-supported shape on SQL today). For file-based connectors leave it null. The query is delivered to the on-prem agent via mothership at every bootstrap, written to `queries/{sourceEntityName}.sql` for the connector to read.\n\n**Schema contract** for the SQL query: project natural-key cols + every data col + a `[Hash]` column from `CONVERT(VARCHAR(40), HASHBYTES('SHA1', (SELECT ... FOR XML RAW)), 2)` with a CORRELATED subquery. Without correlation, every row gets the same hash and dirty detection falls over.\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**Response 200 OK:** `{ entitySyncProfileEntityId: number }`\n\n**View / Report bindings** are pull-only: passing `bindingType=View` or `Report` with `syncDirection != 'Pull'` returns 400."
     },
     "response": []
    },
    {
     "name": "EntitySync — save field map (single mapping)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncFieldMapId\": null,\n  \"entityPropertyId\": 100082,\n  \"sourceFieldName\": \"EmpNo\",\n  \"keyPosition\": 1,\n  \"parentKeyPosition\": null,\n  \"position\": 1,\n  \"transformJson\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/entities/1/fields",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "entities",
        "1",
        "fields"
       ]
      },
      "description": "Create OR update one field map. Replace `1` in the URL with the entitySyncProfileEntityId (the binding id).\n\n**Required**: either `entityPropertyId` (for Entity / Form bindings — maps to a property on the platform entity) OR `bindingColumnName` (for View / Report bindings — references a view-emitted column or a report dotted path like `Customer.OrgName`).\n\n**Composite-key wiring**: pass `keyPosition: 1` for the first key column, `2` for the second, `3` for the third. Pass `parentKeyPosition: 1..3` for parent-relation columns when the binding is a child entity that needs to resolve a parent row.\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**Response 200 OK:** `{ entitySyncFieldMapId: number }`."
     },
     "response": []
    },
    {
     "name": "EntitySync — toggle agent enabled (the trust gate)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"enabled\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agents/1/enabled",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agents",
        "1",
        "enabled"
       ]
      },
      "description": "**The trust gate.** Newly-registered agents start `IsEnabled=false` — admin must explicitly flip this to true before any sync runs. Replace `1` in the URL with the agentId.\n\n**Operationally**: this is the most security-sensitive operation in the EntitySync surface. Granting an agent permission to read + write tenant data is irreversibly impactful. The Portal Agents page surfaces newly-registered (disabled) agents with a 'pending approval' callout so admins notice them.\n\n**Auth:** Bearer + `Roles.AdminUser` (Owner / Site Admin) — *not* `AdminIntegration`. Day-to-day integration admins see the toggle as read-only in the Portal UI.\n\n**Response 200 OK:** `{ entitySyncAgentId: number }`."
     },
     "response": []
    },
    {
     "name": "EntitySync — assign profile to agent",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncProfileId\": 1,\n  \"isEnabled\": true,\n  \"position\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agents/1/profiles",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agents",
        "1",
        "profiles"
       ]
      },
      "description": "Assign a profile to an agent (the many-to-many junction in `EntitySyncAgentProfile`). Replace `1` in the URL with the agentId.\n\nThe sync runs only when ALL THREE of these are true:\n  - `Agent.IsEnabled` (the trust gate, flipped via /enabled)\n  - `AgentProfile.IsEnabled` (this assignment row's flag — set via this body's `isEnabled`)\n  - `Profile.IsEnabled` (the profile's kill switch)\n\n`position` orders multiple profiles assigned to the same agent — the agent runs them sequentially in this order each cycle.\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**Response 200 OK:** `{ entitySyncAgentProfileId: number }`."
     },
     "response": []
    },
    {
     "name": "EntitySync — list runs (paged telemetry)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncProfileId\": null,\n  \"entitySyncAgentId\": null,\n  \"unacknowledgedFailuresOnly\": false,\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/runs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "runs",
        "list"
       ]
      },
      "description": "Per-profile run telemetry, newest-first. Pass `unacknowledgedFailuresOnly: true` to filter to only Failed/PartialFailure/WithErrors runs the operator hasn't acked yet — that's how the dashboard's `/ops/integrations` 'Needs your attention' counter is computed. Default false returns the full history including acked runs (the activity stream's normal view).\n\nResponse rows include `acknowledged` / `acknowledgedBy` / `acknowledgedAtUtc` so the UI can render per-row state."
     },
     "response": []
    },
    {
     "name": "EntitySync — acknowledge runs (clear dashboard counter)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"runIds\": [42, 43, 99]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/runs/acknowledge",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "runs",
        "acknowledge"
       ]
      },
      "description": "Operator-driven workflow so transient sync failures (typo in saved query, missing field maps, network blip) don't leave the dashboard 'Needs your attention' panel red after the cause is fixed. Audit trail preserved — acked runs stay queryable, just stop counting toward the alert.\n\n**Body:** `{ runIds: [number, ...] }` — bulk acknowledge via `dbo.BigIntListTvp`.\n\n**Idempotent**: already-acked rows preserve their original `AcknowledgedBy` / `AcknowledgedAtUtc` audit values. Cross-tenant rows silently no-op via `WHERE TenantId=@TenantId`.\n\n**Response 200 OK:**\n```json\n{ \"acknowledged\": 3 }\n```\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**UI surface:** Activity Stream's per-row green check button + bulk 'Acknowledge all (N)' toolbar both call this endpoint."
     },
     "response": []
    },
    {
     "name": "EntitySync — directives queue (Portal-driven remote-control flow)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncAgentId\": 1,\n  \"entitySyncProfileId\": 9,\n  \"entitySyncProfileEntityId\": 14,\n  \"directiveType\": \"Probe.QueryDescribe\",\n  \"inputJson\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/directives",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "directives"
       ]
      },
      "description": "**Phase 2A.7-B** — queue a directive addressed to one agent. The agent's DirectiveDispatcher claims it at next 30s tick (piggybacked on the heartbeat path), executes locally, posts result back via `/sync/agent/directives/{id}/complete`.\n\n**Asymmetric-network-respecting** — no inbound connectivity required to the agent host.\n\n**Lifecycle**: `Pending` → `Running` (claim) → `Succeeded | Failed`. Portal polls `/sync/directives/{id}` every 2s while a modal is open.\n\n**Body fields:**\n- `entitySyncAgentId` (required): target agent\n- `entitySyncProfileId` / `entitySyncProfileEntityId` (optional): scope. Required for QueryDescribe + AutoCreate; SchemaList runs profile-scoped without binding.\n- `directiveType` (required): one of `Probe.SchemaList` / `Probe.TableColumns` / `Probe.QueryDescribe` / `Probe.AutoCreate`\n- `inputJson` (optional): per-type input parameters as camelCase JSON. Example for TableColumns: `{ \"schema\": \"dbo\", \"table\": \"Employee\" }`. Example for AutoCreate: `{ \"keyColumns\": [\"EmpNo\"], \"mode\": \"AutomaticApprove\", \"languageRegionId\": 1 }`.\n\n**Response 200 OK:**\n```json\n{ \"entitySyncDirectiveId\": 17 }\n```\n\n**Auth:** Bearer + `Roles.AdminIntegration`.\n\n**Portal UI surface:** ProfileEdit page's Probe / Add-all-Missing / Browse SQL buttons all queue via this endpoint."
     },
     "response": []
    },
    {
     "name": "EntitySync — directives detail (Portal poll)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/directives/17",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "directives",
        "17"
       ]
      },
      "description": "Read one directive by id. Drives the Portal's polling loop — call every 2s while `Status` is `Pending` or `Running`. Stop polling when Status flips to `Succeeded` (parse `resultJson`) or `Failed` (display `errorMessage`).\n\n**Response shape:**\n```json\n{\n  \"entitySyncDirectiveId\": 17,\n  \"tenantId\": 1,\n  \"entitySyncAgentId\": 1,\n  \"entitySyncProfileId\": 9,\n  \"entitySyncProfileEntityId\": 14,\n  \"directiveType\": \"Probe.QueryDescribe\",\n  \"inputJson\": null,\n  \"status\": \"Succeeded\",\n  \"resultJson\": \"{\\\"sourceEntityName\\\":\\\"Employee\\\",\\\"entityId\\\":42,\\\"columns\\\":[...]}\",\n  \"errorMessage\": null,\n  \"queuedUtc\": \"2026-05-08T14:30:00Z\",\n  \"claimedUtc\": \"2026-05-08T14:30:18Z\",\n  \"completedUtc\": \"2026-05-08T14:30:19Z\",\n  \"queuedBy\": 5\n}\n```\n\n**Result shapes per DirectiveType:**\n- `Probe.SchemaList`: `{ tables: [{schema, name, modifyDate}, ...], views: [...], tableCount, viewCount }`\n- `Probe.TableColumns`: `{ schema, table, columns: [{name, dataType, rawSourceType, isNullable, maxLength, precision, scale, ordinalPosition}, ...], sampleRows: [[...], ...], sampleRowCount }`\n- `Probe.QueryDescribe`: `{ sourceEntityName, entityId, columns: [{fieldName, inferredType, isNullable, maxLength, precision, scale, rawSourceType}, ...], columnCount }`\n- `Probe.AutoCreate`: `{ bindingsProcessed, succeeded, failed, totalCreated, totalReused, totalSkipped, totalFieldMaps, perBinding: [...], hasFailures }`\n\n**Auth:** Bearer + `Roles.AdminIntegration`."
     },
     "response": []
    },
    {
     "name": "EntitySync — directives list (paged history)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{AcmeApiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncAgentId\": null,\n  \"entitySyncProfileId\": null,\n  \"entitySyncProfileEntityId\": null,\n  \"status\": null,\n  \"sinceUtc\": null,\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/directives/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "directives",
        "list"
       ]
      },
      "description": "Paged tenant-wide list of directives, newest first. Drives an admin diagnostics page (e.g. 'show me all probe directives that failed in the last 24h').\n\n**Filters** (all optional, all narrow the result):\n- `entitySyncAgentId` — one agent's directives\n- `entitySyncProfileId` — one profile\n- `entitySyncProfileEntityId` — one binding\n- `status` — `Pending` / `Running` / `Succeeded` / `Failed` / `Cancelled`\n- `sinceUtc` — only directives queued at or after this UTC timestamp\n\n**Pagination:** `pageNumber` (1-based, default 1), `pageSize` (default 50, capped at 500).\n\n**Auth:** Bearer + `Roles.AdminIntegration`."
     },
     "response": []
    },
    {
     "name": "EntitySync — agent claim directives (X-Agent-Serial)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "X-Agent-Serial",
        "value": "{{AgentSerial}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"maxCount\": 5\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agent/directives/claim",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agent",
        "directives",
        "claim"
       ]
      },
      "description": "**Agent-facing** — atomic `Pending` → `Running` flip via SQL Server `UPDATE TOP(@MaxCount)...OUTPUT...WITH (READPAST, ROWLOCK)`. Returns claimed directive bodies for the agent's DirectiveDispatcher to execute.\n\n**Auth:** `X-Agent-Serial` header (sole credential for agent-facing flows).\n\n**Body:** `{ maxCount: int }` — caps a runaway directive flood. Server hard-caps at 50 regardless.\n\n**Response 200 OK:**\n```json\n{\n  \"directives\": [\n    {\n      \"entitySyncDirectiveId\": 17,\n      \"entitySyncProfileId\": 9,\n      \"entitySyncProfileEntityId\": 14,\n      \"directiveType\": \"Probe.QueryDescribe\",\n      \"inputJson\": null,\n      \"queuedUtc\": \"2026-05-08T14:30:00Z\"\n    }\n  ]\n}\n```\n\nEmpty `directives` array when nothing pending or agent isn't enabled. Disabled agents get empty results (no error) so the heartbeat path stays cheap.\n\nThe agent's DirectiveDispatcher polls this endpoint every 30s in mothership mode."
     },
     "response": []
    },
    {
     "name": "EntitySync — agent complete directive (X-Agent-Serial)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "X-Agent-Serial",
        "value": "{{AgentSerial}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"resultJson\": \"{\\\"sourceEntityName\\\":\\\"Employee\\\",\\\"entityId\\\":42,\\\"columns\\\":[],\\\"columnCount\\\":0}\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agent/directives/17/complete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agent",
        "directives",
        "17",
        "complete"
       ]
      },
      "description": "**Agent-facing** — agent posts the directive's successful result. Server flips `Status` to `Succeeded` + stores `ResultJson` + stamps `CompletedUtc`.\n\n**Auth:** `X-Agent-Serial`.\n\n**Body:** `{ resultJson: string }` — per-type result payload as a JSON string (escape-quoted within the outer JSON).\n\n**Idempotent** — re-posts on already-completed directives return 404, which the agent treats as a no-op (avoids re-execution if a previous post succeeded but the agent didn't see the response).\n\nResult shapes per type — see the directives detail endpoint for the full reference."
     },
     "response": []
    },
    {
     "name": "EntitySync — agent fail directive (X-Agent-Serial)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "X-Agent-Serial",
        "value": "{{AgentSerial}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"errorMessage\": \"saved query syntax error: Invalid object name 'dbo.Employee'\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agent/directives/17/fail",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agent",
        "directives",
        "17",
        "fail"
       ]
      },
      "description": "**Agent-facing** — agent posts an operator-actionable failure message. Server flips `Status` to `Failed` + stores `ErrorMessage` (capped at 2048 chars) + stamps `CompletedUtc`.\n\n**Auth:** `X-Agent-Serial`.\n\n**Body:** `{ errorMessage: string }` — the message Portal renders in red text in the modal. Stack traces should land in `EntitySyncLog` via the agent's LogForwarder, not in this field.\n\n**Idempotent** — re-posts on already-completed directives return 404."
     },
     "response": []
    },
    {
     "name": "EntitySync — health snapshot (operator dashboard)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/health-snapshot",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "health-snapshot"
       ]
      },
      "description": "Returns one row per profile with health metrics: last successful run, last error, drift detection signals, agent checkin recency. Drives the /ops/integrations dashboard (Portal). Backed by a snapshot table maintained by the Health Validator background service."
     },
     "response": []
    },
    {
     "name": "EntitySync — register agent (one-shot per machine)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{AcmeApiKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"agentSerial\": \"00000000-0000-0000-0000-000000000000\",\n  \"displayName\": \"Acme HVAC dispatch sync agent\",\n  \"machineName\": \"ACME-DISPATCH-01\",\n  \"version\": \"1.0.0\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agent/register",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agent",
        "register"
       ]
      },
      "description": "Initial agent registration. The hardware-baked GUID stays in `.agent-state/identity.json` on the agent's machine — survives reinstalls. Returns agentId + initial settings. Admin must explicitly enable the agent in Portal before sync flows (no auto-enable)."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Identity Bridge — Context Engine + TenantUserExternalIdentity",
   "description": "The identity bridge connects synced entity rows (Employee / Tech / Customer with email + phone) to platform users. Read `docs/public/Integrators/Context-Engine-Guide.md` for the full doctrine; this folder is the REST surface every Portal Identity Links UI uses, plus the agent-facing post-import-link path and the Portal-driven import-bound-asset CTA.\n\nFive primitives the bridge runs on:\n  - Entity.IsTenantUserRole (operational-identity flag, multi-flag per tenant)\n  - Property.IsIdentityEmail / IsIdentityPhone (which columns the bridge MERGE proc reads)\n  - TenantUserExternalIdentity (TUEI) row — (TenantUserId NULLABLE, ExternalSystem, ExternalEntityRole, ExternalKey, ExternalEmail, ExternalPhone, LinkProvenance)\n  - EntitySyncProfile.AutoLinkOnSync (sync-time MERGE)\n  - EntitySyncProfile.AutoInviteOnSync (gated on AutoLinkOnSync — creates User + sends invite)\n\nMERGE semantic is skip-if-already-attached: TenantUserId is NEVER overwritten on UPDATE. Re-syncing the same employee row is safe.\n\n**Canonical prefix: /api/v1/identities/*** (these requests use it). The older /api/v1/identity-links/* routes are retained as deprecated aliases (identical handlers). The whole-tenant SWEEP is POST /api/v1/identities/reconcile (AdminIntegration); the per-user targeted reconcile is POST /api/v1/identities/reconcile-user (AdminUser).\n\nAuth: AdminUser (tenant-scoped) for the CRUD family; AdminIntegration for the sweep + import-bound-asset; X-Agent-Serial for post-import-link.",
   "item": [
    {
     "name": "Identity Link — save (create or update; pending or attached)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"externalSystem\": \"ServiceTitan\",\n  \"externalEntityRole\": \"Employee\",\n  \"externalKey\": \"EMP-664\",\n  \"externalEmail\": \"frank@acme.test\",\n  \"externalPhone\": \"717-555-0100\",\n  \"linkProvenance\": \"AdminLinked\",\n  \"linkedTenantUserId\": null\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities"
       ]
      },
      "description": "Create OR update a TenantUserExternalIdentity row.\n\nRequired:\n  - externalSystem (e.g. \"ServiceTitan\", \"ADP\", \"Salesforce\")\n  - externalEntityRole (e.g. \"Employee\", \"Customer\")\n  - externalKey (the source system's id)\n  - linkProvenance (\"AdminLinked\" | \"InviteAccept\" | \"SelfClaimed\" | \"SyncCreated\" | \"AutoMatchedEmail\" | \"AutoMatchedPhone\" | \"PortalReconciled\")\n\nOptional:\n  - tenantUserExternalIdentityId — when set, UPDATE; when omitted/0, INSERT\n  - linkedTenantUserId — set this to attach the bridge to a platform user. NULL = pending row.\n  - externalEmail / externalPhone — captured for invite-time reconciliation\n\n**Sample 200 OK:**\n```json\n{ \"tenantUserExternalIdentityId\": 12345 }\n```\n\n**Sample 409 Conflict (duplicate):**\n```json\n{ \"error\": \"An identity row already exists for these keys.\" }\n```\n\nPRESERVE-ATTACHED guarantee: re-saving an already-attached row (linkedTenantUserId already set) updates email/phone but never RE-binds to a different user — the proc enforces skip-if-already-attached."
     },
     "response": []
    },
    {
     "name": "Identity Link — list (with optional filters)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"externalSystem\": \"ServiceTitan\",\n  \"externalEntityRole\": \"Employee\",\n  \"pendingOnly\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "list"
       ]
      },
      "description": "List identity-link rows in the tenant. All filters are optional — omit the body for everything.\n\n  - externalSystem    — narrow to one source system\n  - externalEntityRole — narrow to one role (Employee / Customer / etc.)\n  - pendingOnly        — when true, only rows with TenantUserId IS NULL\n\n**Sample 200 OK:**\n```json\n[\n  {\n    \"tenantUserExternalIdentityId\": 12345,\n    \"tenantUserId\": null,\n    \"externalSystem\": \"ServiceTitan\",\n    \"externalEntityRole\": \"Employee\",\n    \"externalKey\": \"EMP-664\",\n    \"externalEmail\": \"frank@acme.test\",\n    \"externalPhone\": \"717-555-0100\",\n    \"linkProvenance\": \"SyncCreated\",\n    \"linkedAt\": null,\n    \"createdDate\": \"2026-05-04T03:14:00Z\"\n  }\n]\n```\n\nPortal `/ops/identity-links` page calls this endpoint."
     },
     "response": []
    },
    {
     "name": "Identity Link — detail",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/12345",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "12345"
       ]
      },
      "description": "Single-row detail by tenantUserExternalIdentityId.\n\n**Sample 200 OK:**\n```json\n{\n  \"tenantUserExternalIdentityId\": 12345,\n  \"tenantUserId\": 4,\n  \"externalSystem\": \"ServiceTitan\",\n  \"externalEntityRole\": \"Employee\",\n  \"externalKey\": \"EMP-664\",\n  \"externalEmail\": \"frank@acme.test\",\n  \"externalPhone\": \"717-555-0100\",\n  \"linkProvenance\": \"AcceptInviteReconciled\",\n  \"linkedAt\": \"2026-05-04T15:22:08Z\",\n  \"createdDate\": \"2026-05-04T03:14:00Z\",\n  \"modifiedDate\": \"2026-05-04T15:22:08Z\"\n}\n```\n\n**Sample 404 Not Found:**\n```json\n{ \"error\": \"Identity row not found.\", \"tenantUserExternalIdentityId\": 12345 }\n```"
     },
     "response": []
    },
    {
     "name": "Identity Link — soft-delete",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/12345",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "12345"
       ]
      },
      "description": "Soft-delete (sets ActiveEnd). Hist row preserves the chain for audit.\n\n**Sample 200 OK:**\n```json\n{ \"tenantUserExternalIdentityId\": 12345 }\n```\n\nNote the bridge MERGE proc filters `ActiveEnd > GETUTCDATE()`, so soft-deleted rows don't block re-create on the same (externalSystem, externalEntityRole, externalKey) tuple."
     },
     "response": []
    },
    {
     "name": "Identity Link — by user (per-tenant-user list, PR 8.8.5.2)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/by-user/4",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "by-user",
        "4"
       ]
      },
      "description": "PR 8.8.5.2 - per-tenant-user identity-link list. Path param is the **target** tenantUserId (the user whose bridges you want, NOT the caller). The Portal `/ops/tenant-users/{id}` page's UserIdentitiesDialog calls this endpoint.\n\n**Sample 200 OK:**\n```json\n[\n  {\n    \"tenantUserExternalIdentityId\": 12345,\n    \"externalSystem\": \"ServiceTitan\",\n    \"externalEntityRole\": \"Employee\",\n    \"externalKey\": \"EMP-664\",\n    \"externalEmail\": \"frank@acme.test\",\n    \"linkProvenance\": \"AcceptInviteReconciled\",\n    \"linkedAt\": \"2026-05-04T15:22:08Z\"\n  },\n  {\n    \"tenantUserExternalIdentityId\": 12346,\n    \"externalSystem\": \"ADP\",\n    \"externalEntityRole\": \"PayrollId\",\n    \"externalKey\": \"FN-777\",\n    \"linkProvenance\": \"AdminLinked\",\n    \"linkedAt\": \"2026-05-04T15:30:00Z\"\n  }\n]\n```\n\nReturns an empty array when the user has no bridges (NOT a 404).\n\nAuth: AdminUser. Replaces the prior pattern of `/identity-links/list` + client-side filter — the proc filters server-side."
     },
     "response": []
    },
    {
     "name": "Identity Link — reconcile (manual sweep, PR 8.8.5.2)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"reconciledForTenantUserId\": 4,\n  \"email\": \"frank@acme.test\",\n  \"phone\": \"717-555-0100\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/reconcile-user",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "reconcile-user"
       ]
      },
      "description": "PR 8.8.5.2 - manual reconciliation of ONE known user. Attaches every pending TUEI row matching the supplied email OR phone to the named tenant user. (Canonical route; legacy alias POST /api/v1/identity-links/reconcile still works. NOT the same as the whole-tenant sweep POST /api/v1/identities/reconcile.) Idempotent — re-running with the same inputs reconciles 0 rows.\n\nRequired:\n  - reconciledForTenantUserId — the target user (NOT the caller)\n  - At least one of email / phone (or both)\n\nUse cases:\n  - You manually invited someone via /ops/users (NOT through auto-invite) and want to attach pending bridges they own\n  - The auto-invite orchestrator's existing-user reconcile path failed and you want to retry\n  - Bulk-claim post-merge: a tenant migrated employees from System A to System B (different external keys) but kept emails — reconcile-by-email picks up the new TUEI rows from the System B sync run\n\n**Sample 200 OK:**\n```json\n{ \"reconciledCount\": 3, \"reconciledForTenantUserId\": 4 }\n```\n\n**Sample 400 Bad Request:**\n```json\n{ \"error\": \"reconciledForTenantUserId AND at least one of email/phone are required.\" }\n```\n\n**Sample 404 Not Found:**\n```json\n{ \"error\": \"Target tenant user not found.\", \"reconciledForTenantUserId\": 4 }\n```\n\nThe proc filters `TenantUserId IS NULL` so already-attached rows are NOT re-bound. Skip-if-already-attached semantics apply here as in the sync-driven path."
     },
     "response": []
    },
    {
     "name": "Identity Link — link by id-or-externalKey (#5 convenience)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"identity\": \"EMP-664\",\n  \"tenantUserId\": 4\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/link",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "link"
       ]
      },
      "description": "#5 convenience - attach a bridge row to a platform user by the row's id OR its externalKey, in one call (mirrors the MCP copilot link_identity tool). The raw Save endpoint can also link (set linkedTenantUserId), but it makes you pass the full external tuple; this one resolves the row for you.\n\nRequired:\n  - identity — the row's tenantUserExternalIdentityId (as a string) OR its externalKey (e.g. \"EMP-664\")\n  - tenantUserId — the platform user to attach (get it from resolve_identity / whoami / the users API)\n\nSets LinkProvenance = 'PortalReconciled'. Auth: AdminUser.\n\n**Sample 200 OK:**\n```json\n{ \"linked\": true, \"tenantUserExternalIdentityId\": 12345, \"externalKey\": \"EMP-664\", \"tenantUserId\": 4 }\n```\n\n**Sample 404 Not Found:**\n```json\n{ \"error\": \"No identity link found for 'EMP-664'.\" }\n```"
     },
     "response": []
    },
    {
     "name": "Identity Link — unlink by id-or-externalKey (#5 convenience)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"identity\": \"EMP-664\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/unlink",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "unlink"
       ]
      },
      "description": "#5 convenience - detach a bridge row from its platform user by id OR externalKey (mirrors the MCP copilot unlink_identity tool). Re-pends the row (LinkedTenantUserId -> NULL) so it can re-link / re-invite on the next sweep. Does NOT delete the user. Sets LinkProvenance = 'PortalReconciled'. Auth: AdminUser.\n\n**Sample 200 OK:**\n```json\n{ \"linked\": false, \"tenantUserExternalIdentityId\": 12345, \"externalKey\": \"EMP-664\", \"tenantUserId\": null }\n```"
     },
     "response": []
    },
    {
     "name": "Identity Link — reconcile SWEEP (whole tenant, #655)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/identities/reconcile",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "identities",
        "reconcile"
       ]
      },
      "description": "#655 on-demand SWEEP. Scans ALL pending TUEI rows in the tenant (no platform user yet, with an email) and runs the same reconcile + auto-invite the nightly sync does: a pending row whose email matches an existing tenant user is claimed immediately; an unmatched email gets a new user + tenant membership + an invite email. No body required.\n\nAuth: AdminIntegration (NOT AdminUser - this one can create users + send invites, so it carries the integration scope).\n\n**Sample 200 OK:**\n```json\n{ \"pendingScanned\": 12, \"invitesSent\": 9, \"reconciled\": 2, \"skippedExistingUserNotMember\": 1, \"errors\": [] }\n```\n\nNote: the sweep only sees pending TUEI rows that already exist. Rows authored natively in Data Studio / Forms / the Entity-Data API do not yet auto-create a pending TUEI row (that bridge hook is tracked separately) - sync-imported rows and rows you POST to /api/v1/identities are visible."
     },
     "response": []
    },
    {
     "name": "Sync — Portal import-bound-asset (test the bridge live, PR 8.8.5.1)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"assetKey\": \"00000000-0000-0000-0000-000000000000\",\n  \"tabName\": null,\n  \"treatFirstRowAsData\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/profiles/42/import-bound-asset",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "profiles",
        "42",
        "import-bound-asset"
       ]
      },
      "description": "PR 8.8.5.1 - Portal-driven import. The dream wizard's `Import data now?` CTA calls this endpoint after saving a profile via /from-import. AdminIntegration auth.\n\nPath param: profileId\n\nBody:\n  - assetKey — the GUID returned from POST /api/v1/assets when the wizard uploaded the file\n  - tabName — Excel sheet name (null for CSV/TSV)\n  - treatFirstRowAsData — when true, skip header detection + synthesize Column1/Column2/...\n\nFlow:\n  1. Resolve profile + first enabled Entity binding + binding's field maps\n  2. Read asset bytes via the storage provider\n  3. Parse every row (capped at 1,000,000 rows)\n  4. Translate rows through field maps (Key1/2/3 + ParentKey1/2/3 + canonical EntityPropertyId-keyed DataJson)\n  5. Bulk-save into EntityInstance\n  6. Call EntitySyncPostImportLink to MERGE TenantUserExternalIdentity rows\n  7. Run OrchestrateAutoInvitesAsync (creates Users + sends invite emails for unmatched emails)\n\n**Sample 200 OK (full bridge ran):**\n```json\n{\n  \"rowsParsed\": 50,\n  \"inserted\": 48,\n  \"updated\": 0,\n  \"skipped\": 2,\n  \"conflicted\": 0,\n  \"keylessSkipped\": 0,\n  \"truncated\": false,\n  \"unmappedSourceFields\": [],\n  \"linksTouched\": 50,\n  \"pendingCount\": 50,\n  \"invitesSent\": 46,\n  \"invitesSkippedExistingUser\": 2,\n  \"invitesReconciled\": 2,\n  \"identityBridgeRan\": true,\n  \"errors\": []\n}\n```\n\n**Sample 200 OK (entity isn't IsTenantUserRole — no bridge):**\n```json\n{\n  \"rowsParsed\": 50,\n  \"inserted\": 50,\n  \"updated\": 0,\n  \"linksTouched\": 0,\n  \"identityBridgeRan\": false,\n  \"errors\": []\n}\n```\n\n**Sample 400 Bad Request:**\n```json\n{ \"error\": \"Profile binding has no field maps - nothing to import.\" }\n```"
     },
     "response": []
    },
    {
     "name": "Sync — Agent post-import-link (X-Agent-Serial, PR 8.8.5)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "X-Agent-Serial",
        "value": "{{agentSerial}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"entitySyncProfileEntityId\": 7,\n  \"syncedKeys\": [\n    { \"key1\": \"EMP-664\", \"key2\": \"\", \"key3\": \"\" },\n    { \"key1\": \"EMP-665\", \"key2\": \"\", \"key3\": \"\" },\n    { \"key1\": \"EMP-666\", \"key2\": \"\", \"key3\": \"\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/sync/agent/post-import-link",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "sync",
        "agent",
        "post-import-link"
       ]
      },
      "description": "PR 8.8.5 - the agent's post-bulk-save bridge call. After every successful push of a binding's rows, when the binding's profile carries AutoLinkOnSync=true, the on-prem agent calls this endpoint with the just-pushed natural keys.\n\nAuth: X-Agent-Serial header (NOT Bearer). The agent's serial is generated on first run + stored in `appsettings.json` + reported on every register/checkin.\n\nRequest:\n  - entitySyncProfileEntityId — which binding produced these keys\n  - syncedKeys — array of composite-key tuples\n\nFlow:\n  1. Resolve agent → tenant via the tenant-factory linear scan\n  2. Call EntitySyncPostImportLink (MERGE TenantUserExternalIdentity, skip-if-already-attached)\n  3. For each PendingInvites row → OrchestrateAutoInvitesAsync (existing-user reconcile OR create User + invite)\n  4. Lang/TZ resolution chain: profile override → tenant default → English / agent IANA / Eastern US\n\n**Sample 200 OK:**\n```json\n{\n  \"linksTouched\": 3,\n  \"pendingCount\": 3,\n  \"invitesSent\": 2,\n  \"invitesSkippedExistingUser\": 1,\n  \"invitesReconciled\": 1,\n  \"errors\": []\n}\n```\n\n**Sample 401 (agent not registered):**\n```json\n{ \"error\": \"Agent serial not registered.\" }\n```\n\nIntegrators forking the agent typically don't call this directly — `EntitySyncer.cs` does it after every successful binding push when `EntitySyncDefinition.AutoLinkOnSync` is true."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Context Engine — ContextDefinition CRUD + Hydrate",
   "description": "The Context Engine is the platform's universal data-context layer. Tenants author `ContextDefinition`s declaratively (anchor entity + bindings + parameters); every consumer surface (workflows, RBM/email templates, customer portals, mobile pickers, AI Copilot, forms) consumes the hydrated `Bundle` shape via one `[[ ]]` token resolver.\n\nRead `docs/public/Integrators/Context-Engine-Guide.md` for the operator-facing doctrine, or `docs/claude/CONTEXT-ENGINE.md` for the developer-depth reference.\n\nFour CRUD endpoints + two Hydrate endpoints. CRUD requires BuilderAdmin (context defs are tenant schema). Hydrate requires DataView OR DataAdmin (read-data, not author-schema).\n\n---\n\n## Authoring contexts — the `bindingsJson` shape primer\n\nA ContextDefinition is metadata + one field that does the work: **`bindingsJson`**, a JSON **string** (anchor + parameters + bindings). **All keys are camelCase and case-sensitive** — a mis-cased key is silently ignored (treated as absent). See the **\"Create a Context (authoring — Acme_VisitContext)\"** request in this folder for a fully-annotated worked example.\n\n### Skeleton\n\n```jsonc\n{\n  \"version\": 1,                       // int, optional (default 1)\n  \"anchor\": {                         // REQUIRED — the one row this context is about\n    \"name\": \"Visit\",                  // REQUIRED — bundle slot key + token namespace: [[Visit.Field]]\n    \"entityRole\": \"WorkOrderVisit\",   // tenant entity InternalName (required for rowByKey/rowById)\n    \"kind\": \"rowByKey\",               // rowByKey | rowById | currentUser | currentTenantUser | currentCustomer\n    \"identityFields\": [               // REQUIRED for rowByKey — maps each key field to its composite slot (1/2/3)\n      { \"field\": \"DispatchNumber\", \"keyPosition\": 1 },\n      { \"field\": \"EmployeeIdRef\",  \"keyPosition\": 2 },\n      { \"field\": \"VisitCounter\",   \"keyPosition\": 3 }\n    ],\n    \"expects\": \"single\"               // always \"single\" — anchors are single-row\n  },\n  \"parameters\": [                     // optional — caller-passable scope values, read as $.name\n    { \"name\": \"asOf\", \"type\": \"datetime\", \"required\": false, \"default\": \"[[today]]\", \"enum\": null }\n  ],\n  \"bindings\": [                       // optional — the related things you reach; order doesn't matter (topo-sorted)\n    {\n      \"name\": \"Customer\",             // REQUIRED — bundle slot key + token namespace\n      \"source\": \"compositeKey\",       // REQUIRED — one of the 10 sources (table below)\n      \"entityRole\": \"Customer\",       // target entity InternalName (most sources)\n      \"expects\": \"single\",            // single | many | list-source — must be compatible with source\n      \"fields\": [\"OrgName\",\"OrgPhone\"],// projection; null/omit = full row\n      \"via\": { \"key1\": \"[[WorkOrder.CustomerNumber]]\" },  // compositeKey/rowById: token-fed key slots\n      \"scope\": \"[[ServiceLocation]]\", // child/parent: which already-resolved sibling row is the parent (two-level reach)\n      \"filter\": { \"field\": \"Status\", \"op\": \"equals\", \"value\": \"Open\" },  // child/entity/view narrowing\n      \"orderBy\": [ { \"field\": \"OrgName\", \"asc\": true } ],// many/list-source\n      \"limit\": 200,                   // row cap for many/list-source\n      \"viewId\": null,                 // view source only\n      \"reportName\": null,             // report source (or reportId; id wins)\n      \"keyField\": null, \"displayField\": null,  // list-source picker (stored value / label)\n      \"expandFields\": [               // entity+many only: per-row JOINs without N+1 (declared order = multi-hop)\n        { \"from\": \"Employee\", \"match\": { \"EmployeeIdRef\": \"EmployeeId\" }, \"project\": [\"FirstName\",\"LastName\",\"Phone\"] }\n      ]\n    }\n  ]\n}\n```\n\n### The 10 binding sources\n\n| `source` | Reaches | Required | Cardinality |\n|---|---|---|---|\n| `compositeKey` | one row by composite key, fed from tokens | `entityRole`, `via` (`key1`[,`key2`,`key3`]) | `single` |\n| `rowById` | one row by `EIRecordId` from a token | `entityRole`, `via.eiRecordId` | `single` |\n| `parent` | the anchor's parent (via `ParentEIRecordId`) | `entityRole` | `single` |\n| `child` | child rows of the anchor — or of a **sibling** via `scope` | `entityRole` (+ `scope?`, `filter?`) | `many` / `list-source` |\n| `entity` | rows across a whole entity, filtered | `entityRole` (+ `filter?`, `expandFields?`) | `single` (first match) / `many` / `list-source` |\n| `view` | rows from a saved EntityInstanceView (picker) | `viewId` | `list-source` |\n| `report` | rows from a saved cross-entity ReportDefinition | `reportId` **or** `reportName` | `many` |\n| `produced` / `context` / `formula` | reserved — parse OK but **throw at hydrate** | — | — don't ship |\n\n### `via`, `scope`, `filter` — the moving parts\n\n- **`via`** (compositeKey/rowById) maps key slots to `[[ ]]` token expressions resolved against the in-progress bundle: `\"via\": { \"key1\": \"[[Visit.WorkOrderNumber]]\" }`. `key1` is required.\n- **`scope`** (child/parent) reaches off a **sibling** instead of the anchor: `\"scope\": \"[[ServiceLocation]]\"` = \"equipment at the work order's location,\" not \"directly on the work order.\" The token must resolve to a row (its `EIRecordId` is read).\n- **`filter`** (child/entity/view) is a single rule `{ field, op, value }` or an AND/OR group `{ logic, rules[] }`, nestable. `value` may be a `[[ ]]` token. **Use the canonical ops** — `equals · notEquals · gt · lt · gte · lte · before · after · contains · startsWith · endsWith`. ⚠ The hydrate path does **not** normalize aliases: write `equals`, not `eq` (an unrecognized op silently degrades to a `contains` LIKE). `isNull`/`in` are unsupported.\n\n### Tokens — `[[ ]]` and the ambients\n\nRoot resolves **ambients first, then the bundle**. Always-available ambients: `[[today]]`, `[[now]]`, `[[currentUser.tenantUserId]]`, `[[currentUser.language]]`, `[[currentUser.identities.<System>.<Role>]]` (bridged external key — what a `currentTenantUser` worklist filters on), `[[tenant.name]]`, `[[tenant.defaultLanguageRegionId]]`. Keys are camelCase. Whitelisted helpers: `length · count · upper · lower · trim · ageInDays · date · ifnull · any · all`. Parameters are `$.`-prefixed.\n\n> This is the same repeatable pattern as the Forms folder: **shape skeleton at the folder level** here, plus one fully-annotated worked request (Create-a-Context) and the hydrate payoff. The full element-by-element reference is `docs/public/Integrators/Context-API.md`.",
   "item": [
    {
     "name": "Context — recommend (what contexts to build)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"anchorEntity\": \"WorkOrder\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/recommend",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "recommend"
       ]
      },
      "description": "Ask the platform what contexts the tenant should build (#27). It reads the schema (entities + key columns + parent/child graph), finds the most-connected entities, and returns ranked, ready-to-save ContextDefinitions — each with an anchor, binding chips, and the full `bindingsJson`. 100% SQL/C#, no AI; already-built anchors are skipped. BuilderAdmin gated.\n\n`anchorEntity` is OPTIONAL — omit (send `{}`) to get every recommended context across the tenant.\n\n**The no-grammar author loop:** recommend → take a result's internalName/displayName/anchorKind/anchorEntityId/bindingsJson → POST /api/v1/context (save) → hydrate.\n\n**Sample 200 OK:**\n```json\n{\n  \"recommendations\": [\n    {\n      \"internalName\": \"WorkOrderContext\",\n      \"displayName\": \"Work Order Context\",\n      \"anchorRole\": \"WorkOrder\",\n      \"anchorEntityId\": 100015,\n      \"anchorKind\": \"rowByKey\",\n      \"score\": 12,\n      \"bindings\": [ { \"role\": \"Customer\", \"kind\": \"CompositeKey\", \"cardinality\": \"Single\" } ],\n      \"bindingsJson\": \"{...}\"\n    }\n  ]\n}\n```\n\nThe same capability is the `recommend_contexts` Copilot/MCP tool (POST /api/CopilotChat), so an AI agent walks the identical loop."
     },
     "response": []
    },
    {
     "name": "Context — save ContextDefinition (create or update)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Acme_DispatchContext\",\n  \"displayName\": \"Dispatch Context\",\n  \"description\": \"Everything a dispatcher needs for one work order.\",\n  \"anchorKind\": \"rowByKey\",\n  \"bindingsJson\": \"{\\\"version\\\":1,\\\"anchor\\\":{\\\"name\\\":\\\"Job\\\",\\\"entityRole\\\":\\\"WorkOrder\\\",\\\"kind\\\":\\\"rowByKey\\\",\\\"identityFields\\\":[{\\\"field\\\":\\\"WorkOrderNumber\\\",\\\"keyPosition\\\":1}],\\\"expects\\\":\\\"single\\\"},\\\"parameters\\\":[],\\\"bindings\\\":[{\\\"name\\\":\\\"ServiceLocation\\\",\\\"source\\\":\\\"compositeKey\\\",\\\"entityRole\\\":\\\"ServiceLocation\\\",\\\"expects\\\":\\\"single\\\",\\\"via\\\":{\\\"key1\\\":\\\"[[Job.CustomerNumber]]\\\",\\\"key2\\\":\\\"[[Job.LocationNumber]]\\\"}},{\\\"name\\\":\\\"Customer\\\",\\\"source\\\":\\\"compositeKey\\\",\\\"entityRole\\\":\\\"Customer\\\",\\\"expects\\\":\\\"single\\\",\\\"via\\\":{\\\"key1\\\":\\\"[[Job.CustomerNumber]]\\\"}},{\\\"name\\\":\\\"Equipment\\\",\\\"source\\\":\\\"child\\\",\\\"entityRole\\\":\\\"Equipment\\\",\\\"expects\\\":\\\"many\\\",\\\"scope\\\":\\\"[[ServiceLocation]]\\\",\\\"limit\\\":200}]}\",\n  \"parametersJson\": null,\n  \"versionNumber\": 1,\n  \"isKitContent\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context"
       ]
      },
      "description": "Create or update a `WorkflowContextDefinition`. BuilderAdmin gated.\n\nRequired:\n  - internalName (PascalCase, unique per tenant)\n  - anchorKind ('named' or 'slot' — controls how anchor identity values are passed at hydrate time)\n  - bindingsJson — string-escaped JSON with the full binding tree (anchor + bindings + parameters)\n\nOptional:\n  - id — set to update; omit/0 to create\n  - displayName, description\n  - anchorEntityId — concrete EntityId for the anchor (NULL when role-mapped via a Kit)\n  - parametersJson — declared scope variables\n  - versionNumber, isKitContent, sourceKitName, sourceKitVersion — for Kit-shipped definitions\n\n**Sample 200 OK:**\n```json\n{ \"id\": 42, \"internalName\": \"JobDailyContext\" }\n```\n\n**Sample 409 Conflict:**\n```json\n{ \"error\": \"A definition with this InternalName already exists.\", \"internalName\": \"JobDailyContext\" }\n```\n\nFor the bindingsJson grammar (anchor + bindings + sources: parent / child / lookup / view / report / live), see `docs/claude/CONTEXT-ENGINE.md` §5."
     },
     "response": []
    },
    {
     "name": "Context — list ContextDefinitions",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "list"
       ]
      },
      "description": "List every live ContextDefinition in the tenant. Empty body OK.\n\n**Sample 200 OK:**\n```json\n{\n  \"definitions\": [\n    {\n      \"workflowContextDefinitionId\": 42,\n      \"internalName\": \"JobDailyContext\",\n      \"displayName\": \"Job Daily Context\",\n      \"anchorEntityId\": 100123,\n      \"anchorKind\": \"named\",\n      \"versionNumber\": 1,\n      \"isKitContent\": false,\n      \"createdDate\": \"2026-04-15T10:00:00Z\"\n    }\n  ]\n}\n```\n\nThe Designer pane in Data Studio (PR 2b) calls this endpoint to render the definition picker."
     },
     "response": []
    },
    {
     "name": "Context — detail by internalName (full BindingsJson)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/Acme_DispatchContext",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "Acme_DispatchContext"
       ]
      },
      "description": "Fetch a ContextDefinition by InternalName (or numeric id). Returns the full row including BindingsJson + ParametersJson — what the Designer needs for editing.\n\nPath param: definition's InternalName (e.g. `JobDailyContext`) OR numeric id (e.g. `42`).\n\n**Sample 200 OK:**\n```json\n{\n  \"definition\": {\n    \"workflowContextDefinitionId\": 42,\n    \"internalName\": \"JobDailyContext\",\n    \"displayName\": \"Job Daily Context\",\n    \"anchorEntityId\": 100123,\n    \"anchorKind\": \"named\",\n    \"bindingsJson\": \"{...full JSON...}\",\n    \"parametersJson\": null,\n    \"versionNumber\": 1,\n    \"isKitContent\": false\n  }\n}\n```\n\n**Sample 404:**\n```json\n{ \"error\": \"Definition not found.\", \"name\": \"JobDailyContext\" }\n```"
     },
     "response": []
    },
    {
     "name": "Context — delete ContextDefinition",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/42",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "42"
       ]
      },
      "description": "Soft-delete a ContextDefinition by numeric id. Hist row preserves the chain.\n\n**Sample 204 No Content** (no body).\n\n**Sample 404:**\n```json\n{ \"error\": \"Definition not found or already deleted.\", \"id\": 42 }\n```\n\nWorkflow steps and email templates that reference the deleted definition by name will fail at hydrate time with a 404 — no implicit cascade."
     },
     "response": []
    },
    {
     "name": "Context — hydrate (anchor identity → Bundle)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"anchorIdentity\": {\n    \"WorkOrderNumber\": \"WO-5001\"\n  },\n  \"parameters\": {\n    \"asOf\": \"2026-06-05\"\n  },\n  \"languageRegionId\": 69\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/Acme_DispatchContext/hydrate?pack=false",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "Acme_DispatchContext",
        "hydrate"
       ],
       "query": [
        {
         "key": "pack",
         "value": "false"
        }
       ]
      },
      "description": "**The universal hydrate endpoint.** Resolve a ContextDefinition into a Bundle for any caller that has an anchor identity. DataView / DataAdmin gated.\n\nPath param: ContextDefinition's InternalName.\n\nQuery: `pack=true` strips internal metadata for wire-efficient size; `pack=false` (default) returns the full debug-friendly shape.\n\nBody:\n  - anchorIdentity — named-key dict (named anchor) or { key1, key2, key3 } slot form (slot anchor)\n  - parameters — declared scope variables (matches the def's parametersJson)\n  - ambientOverrides — override platform ambients (`today`, `now`, etc.) for testing / time-travel\n  - languageRegionId — for the bundle's display-language fallback\n\n**Sample 200 OK:**\n```json\n{\n  \"data\": {\n    \"WorkOrder\": { \"JobNo\": \"DSP-9001\", \"ScheduledDate\": \"2026-04-15\" },\n    \"Customer\": { \"OrgName\": \"Acme HVAC\", \"DispatchPhone\": \"555-1234\" },\n    \"AssignedTechs\": [\n      { \"TechName\": \"Frank\", \"Email\": \"frank@acme.test\", \"TenantUserId\": 4 }\n    ]\n  },\n  \"meta\": {\n    \"definition\": \"JobDailyContext\",\n    \"anchor\": { \"WorkOrderNumber\": \"DSP-9001\" },\n    \"parameters\": { \"asOf\": \"2026-04-15\" },\n    \"ambients\": { \"today\": \"2026-04-15\", \"now\": \"2026-04-15T14:30:00Z\" }\n  }\n}\n```\n\n**Sample 404:**\n```json\n{ \"error\": \"ContextDefinition not found.\", \"definitionName\": \"JobDailyContext\" }\n```\n\n**Sample 422 (def malformed):**\n```json\n{ \"error\": \"ContextDefinition is malformed.\", \"detail\": \"...\" }\n```\n\nBundle size lints: warn at 100KB, error at 500KB. Identity-bridged entities (IsTenantUserRole=true) surface `TenantUserId` + bridged-user metadata alongside entity properties."
     },
     "response": []
    },
    {
     "name": "Context — hydrate-on-instance (workflow runner)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"contextDefName\": \"JobDailyContext\",\n  \"anchorIdentity\": {\n    \"WorkOrderNumber\": \"DSP-9001\"\n  },\n  \"parameters\": {},\n  \"languageRegionId\": 69\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/runner/instances/123/hydrate-context",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "runner",
        "instances",
        "123",
        "hydrate-context"
       ]
      },
      "description": "Workflow-instance-scoped hydrate. Same Engine as `/api/v1/context/{name}/hydrate` but the result is also persisted onto the workflow instance's MetaJson + PickerJson. Step text + branches resolve `[[ ]]` tokens against this cached bundle for the instance's lifetime.\n\nPath param: `workflowInstanceId`.\n\nBody:\n  - contextDefName (REQUIRED) — which definition to hydrate\n  - anchorIdentity, parameters, ambientOverrides, languageRegionId — same as universal hydrate\n\n**Sample 200 OK:**\n```json\n{\n  \"workflowInstanceId\": 123,\n  \"bundle\": { \"data\": {...}, \"meta\": {...} },\n  \"persistedToInstance\": true,\n  \"warnings\": []\n}\n```\n\nUsed by the Workflow Runner at instance start (PR 4) so subsequent step text rendering is offline-cacheable + branch evaluation is bundle-driven. Field techs running offline (PR 6) read from this cached bundle without round-tripping."
     },
     "response": []
    },
    {
     "name": "Create a Context (authoring — Acme_VisitContext)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"internalName\": \"Acme_VisitContext\",\n  \"displayName\": \"Visit 360\",\n  \"description\": \"Everything around one work-order visit: the parent work order, the assigned technician, the customer + service location reached through the work order, the equipment at that location, and the visit's siblings on the same job.\",\n  \"anchorKind\": \"rowByKey\",\n  \"bindingsJson\": \"{\\\"version\\\": 1, \\\"anchor\\\": {\\\"name\\\": \\\"Visit\\\", \\\"entityRole\\\": \\\"WorkOrderVisit\\\", \\\"kind\\\": \\\"rowByKey\\\", \\\"identityFields\\\": [{\\\"field\\\": \\\"DispatchNumber\\\", \\\"keyPosition\\\": 1}, {\\\"field\\\": \\\"EmployeeIdRef\\\", \\\"keyPosition\\\": 2}, {\\\"field\\\": \\\"VisitCounter\\\", \\\"keyPosition\\\": 3}], \\\"expects\\\": \\\"single\\\"}, \\\"parameters\\\": [], \\\"bindings\\\": [{\\\"name\\\": \\\"WorkOrder\\\", \\\"source\\\": \\\"compositeKey\\\", \\\"entityRole\\\": \\\"WorkOrder\\\", \\\"expects\\\": \\\"single\\\", \\\"via\\\": {\\\"key1\\\": \\\"[[Visit.WorkOrderNumber]]\\\"}, \\\"fields\\\": [\\\"WorkOrderNumber\\\", \\\"ServiceType\\\", \\\"CustomerNumber\\\", \\\"LocationNumber\\\", \\\"Status\\\"]}, {\\\"name\\\": \\\"Technician\\\", \\\"source\\\": \\\"compositeKey\\\", \\\"entityRole\\\": \\\"Employee\\\", \\\"expects\\\": \\\"single\\\", \\\"via\\\": {\\\"key1\\\": \\\"[[Visit.EmployeeIdRef]]\\\"}, \\\"fields\\\": [\\\"EmployeeId\\\", \\\"FirstName\\\", \\\"LastName\\\", \\\"Phone\\\", \\\"Zone\\\"]}, {\\\"name\\\": \\\"Customer\\\", \\\"source\\\": \\\"compositeKey\\\", \\\"entityRole\\\": \\\"Customer\\\", \\\"expects\\\": \\\"single\\\", \\\"via\\\": {\\\"key1\\\": \\\"[[WorkOrder.CustomerNumber]]\\\"}, \\\"fields\\\": [\\\"OrgName\\\", \\\"OrgPhone\\\"]}, {\\\"name\\\": \\\"ServiceLocation\\\", \\\"source\\\": \\\"compositeKey\\\", \\\"entityRole\\\": \\\"ServiceLocation\\\", \\\"expects\\\": \\\"single\\\", \\\"via\\\": {\\\"key1\\\": \\\"[[WorkOrder.CustomerNumber]]\\\", \\\"key2\\\": \\\"[[WorkOrder.LocationNumber]]\\\"}}, {\\\"name\\\": \\\"Equipment\\\", \\\"source\\\": \\\"child\\\", \\\"entityRole\\\": \\\"Equipment\\\", \\\"expects\\\": \\\"many\\\", \\\"limit\\\": 200, \\\"scope\\\": \\\"[[ServiceLocation]]\\\"}, {\\\"name\\\": \\\"SiblingVisits\\\", \\\"source\\\": \\\"entity\\\", \\\"entityRole\\\": \\\"WorkOrderVisit\\\", \\\"expects\\\": \\\"many\\\", \\\"limit\\\": 200, \\\"filter\\\": {\\\"field\\\": \\\"WorkOrderNumber\\\", \\\"op\\\": \\\"equals\\\", \\\"value\\\": \\\"[[Visit.WorkOrderNumber]]\\\"}, \\\"expandFields\\\": [{\\\"from\\\": \\\"Employee\\\", \\\"match\\\": {\\\"EmployeeIdRef\\\": \\\"EmployeeId\\\"}, \\\"project\\\": [\\\"FirstName\\\", \\\"LastName\\\", \\\"Phone\\\"]}]}]}\",\n  \"parametersJson\": null,\n  \"versionNumber\": 1,\n  \"isKitContent\": false\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context"
       ]
      },
      "description": "**Author a ContextDefinition over HTTP — `POST /api/v1/context`** (role `BuilderAdmin`).\n\nA ContextDefinition is mostly metadata plus one field that does all the work: **`bindingsJson`**, a JSON **string** carrying the whole map (anchor + parameters + bindings). This example is modeled on **`Acme_VisitContext`** (\"Visit 360\"), anchored on `WorkOrderVisit`.\n\nRead it as a map — **start at the visit, then reach out:**\n- **anchor `Visit`** — the one row this context is about, identified by the visit's 3-part composite key (`DispatchNumber` / `EmployeeIdRef` / `VisitCounter`).\n- **`WorkOrder`** — the parent work order, looked up by the key *on the visit* (`[[Visit.WorkOrderNumber]]`).\n- **`Technician`** — the assigned `Employee`, looked up by `[[Visit.EmployeeIdRef]]`.\n- **`Customer`** + **`ServiceLocation`** — reached by keys on a **sibling** (`[[WorkOrder.CustomerNumber]]` / `[[WorkOrder.LocationNumber]]`), not on the visit. This is the two-level reach: a token can resolve against any already-resolved binding.\n- **`Equipment`** — `child` rows scoped to the **service location** (`scope: \"[[ServiceLocation]]\"`) — \"equipment at the visit's location,\" reaching two hops out.\n- **`SiblingVisits`** — every visit on the same job (`entity` + `filter`), each row folding its technician's name in via `expandFields` (a batched JOIN, no N+1).\n\n> If `Acme_VisitContext` already exists in your tenant, a create collides (`409`) — change `internalName`, or pass the existing `id` to update it.\n\n---\n\n### Top-level body (outside `bindingsJson`)\n\n| Field | Type | Required | Meaning |\n|---|---|---|---|\n| `id` | long | no (omit/0 = create) | Pass an existing `WorkflowContextDefinitionId` to update. |\n| `internalName` | string | **yes** | Unique per tenant (PascalCase). How you address the definition (hydrate path, list, detail). |\n| `displayName` | string | no | Friendly label for the list view. |\n| `description` | string | no | Free text. |\n| `anchorKind` | string | no | Metadata for the list view — the **authoritative** kind is `anchor.kind` *inside* `bindingsJson`. |\n| `anchorEntityId` | long | no | Optional; the engine resolves the entity from the anchor's `entityRole` at hydrate. |\n| `bindingsJson` | string | **yes** | The binding tree (below), escaped as a string. |\n| `versionNumber` | int | no | Definition version. |\n| `isKitContent` | bool | no | True only for Workflow-Kit-sourced definitions. |\n\nThe complete `bindingsJson` element-by-element reference — every anchor kind, all 10 binding sources, `via` / `scope` / `filter` / `expandFields` / `list-source` — lives on this folder's description and in `docs/public/Integrators/Context-API.md`.\n\n**Errors:** `409` InternalName exists · `404` updating a missing id · `401` unauthorized. (Field-level validity is checked at **hydrate**, not create — create only requires structurally-valid JSON.)"
     },
     "response": [
      {
       "name": "200 OK — saved",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 100043,\n  \"internalName\": \"Acme_VisitContext\"\n}"
      },
      {
       "name": "409 Conflict — InternalName already exists",
       "status": "Conflict",
       "code": 409,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"A ContextDefinition named 'Acme_VisitContext' already exists. Pass its id to update, or choose a new internalName.\"\n}"
      }
     ]
    },
    {
     "name": "Hydrate a Context (Acme_VisitContext → Bundle)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"anchorIdentity\": {\n    \"DispatchNumber\": \"DSP-9001\",\n    \"EmployeeIdRef\": \"E001\",\n    \"VisitCounter\": \"1\"\n  },\n  \"parameters\": {},\n  \"ambientOverrides\": null,\n  \"languageRegionId\": 69\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/context/Acme_VisitContext/hydrate?pack=false&cache=true",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "context",
        "Acme_VisitContext",
        "hydrate"
       ],
       "query": [
        {
         "key": "pack",
         "value": "false"
        },
        {
         "key": "cache",
         "value": "true"
        }
       ]
      },
      "description": "**Hydrate `Acme_VisitContext` against one visit — `POST /api/v1/context/{name}/hydrate`** (role `DataView` or `DataAdmin`).\n\nResolve the definition into a **Bundle** for the visit identified by `anchorIdentity`. Every consumer surface (workflows, email/RBM, surfaces, forms, AI agents) reads the same bundle with `[[ ]]` tokens — e.g. `[[Customer.OrgName]]`, `[[Technician.FirstName]]`, `[[Equipment.length]]`, `[[SiblingVisits[0].FirstName]]`, `[[ServiceLocation.City]]`.\n\n**Path param:** the definition's `InternalName` (numeric id also works).\n\n**Query:** `?cache=true` (default — serve a warm bundle when no source moved; `false` forces a fresh hydrate). `?pack=true` adds a gzip+base64 `pack` block for offline shipping.\n\n**Body (all fields optional):**\n- `anchorIdentity` — the key(s) the anchor needs. Accepts named keys (from `anchor.identityFields`, preferred), or `{ eiRecordId }`, or raw `{ key1, key2, key3 }`, or `{ externalKey }`. Omit for `currentUser`-kind anchors (the session is the anchor).\n- `parameters` — values for declared parameters, keyed by name. Omit to use each default.\n- `ambientOverrides` — mask ambient tokens for testing / time-travel, e.g. `{ \"today\": \"2026-01-01\" }`.\n- `languageRegionId` — display language for the resolved data (defaults to English).\n\n**The response envelope** (see the saved 200 example): `data` is keyed by the anchor `name` (`Visit`) + every binding `name` — `single` → object (or null), `many` → array, `list-source` → picker envelope. Every row carries `EIRecordId` + key fields alongside its friendly-named properties; `expandFields` projections (here the technician's `FirstName`/`LastName`/`Phone`) appear as extra keys on each `SiblingVisits` row. `tokens` are the resolved ambients; `issues[]` is empty when everything resolved (a binding that found nothing is an empty object/array, not an issue); `size` is the weight + lint severity.\n\n**Errors:** `404` definition or anchor row not found · `422` definition malformed or parameter validation failed · `500` binding cycle, or a binding used a not-yet-supported source (`produced`/`context`/`formula`)."
     },
     "response": [
      {
       "name": "200 OK — the bundle envelope",
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"definitionName\": \"Acme_VisitContext\",\n  \"definitionVersion\": 1,\n  \"kitName\": null,\n  \"kitVersion\": null,\n  \"anchorEntity\": \"Visit\",\n  \"anchorEIRecordId\": 7700,\n  \"hydratedAt\": \"2026-06-07T12:00:00.000Z\",\n  \"tokens\": {\n    \"today\": \"2026-06-07T00:00:00Z\",\n    \"now\": \"2026-06-07T12:00:00Z\",\n    \"currentUser\": {\n      \"tenantUserId\": 1337,\n      \"language\": \"en-US\",\n      \"languageRegionId\": 69\n    },\n    \"currentTenant\": {\n      \"tenantId\": 1\n    },\n    \"tenant\": {\n      \"tenantId\": 1,\n      \"name\": \"Acme\",\n      \"subDomain\": \"acme\",\n      \"defaultLanguage\": \"en-US\",\n      \"defaultLanguageRegionId\": 69\n    }\n  },\n  \"parameters\": {},\n  \"data\": {\n    \"Visit\": {\n      \"DispatchNumber\": \"DSP-9001\",\n      \"EmployeeIdRef\": \"E001\",\n      \"VisitCounter\": \"1\",\n      \"WorkOrderNumber\": \"WO-5001\",\n      \"VisitStatus\": \"Working\",\n      \"WorkStartDateTime\": \"2026-06-07T14:05:00Z\",\n      \"EIRecordId\": 7700,\n      \"Key1\": \"DSP-9001\",\n      \"Key2\": \"E001\",\n      \"Key3\": \"1\",\n      \"ExternalKey\": \"DSP-9001 / E001 / 1\"\n    },\n    \"WorkOrder\": {\n      \"WorkOrderNumber\": \"WO-5001\",\n      \"ServiceType\": \"Repair\",\n      \"CustomerNumber\": \"CUST-001\",\n      \"LocationNumber\": \"LOC-001\",\n      \"Status\": \"Open\",\n      \"EIRecordId\": 5001\n    },\n    \"Technician\": {\n      \"EmployeeId\": \"E001\",\n      \"FirstName\": \"Maria\",\n      \"LastName\": \"Reyes\",\n      \"Phone\": \"860-555-0101\",\n      \"Zone\": \"Hartford-North\",\n      \"EIRecordId\": 8821\n    },\n    \"Customer\": {\n      \"OrgName\": \"Eastern Plaza Holdings\",\n      \"OrgPhone\": \"860-555-0001\",\n      \"EIRecordId\": 3001\n    },\n    \"ServiceLocation\": {\n      \"AddressLine1\": \"123 Main St\",\n      \"City\": \"Hartford\",\n      \"State\": \"CT\",\n      \"EIRecordId\": 4100\n    },\n    \"Equipment\": [\n      {\n        \"EquipmentNumber\": \"EQ-001\",\n        \"EquipmentMake\": \"Carrier\",\n        \"EquipmentModel\": \"30RB-040\",\n        \"EIRecordId\": 9001\n      }\n    ],\n    \"SiblingVisits\": [\n      {\n        \"DispatchNumber\": \"DSP-9001\",\n        \"VisitCounter\": \"1\",\n        \"WorkOrderNumber\": \"WO-5001\",\n        \"VisitStatus\": \"Working\",\n        \"EmployeeIdRef\": \"E001\",\n        \"FirstName\": \"Maria\",\n        \"LastName\": \"Reyes\",\n        \"Phone\": \"860-555-0101\",\n        \"EIRecordId\": 7700\n      }\n    ]\n  },\n  \"issues\": [],\n  \"size\": {\n    \"plainBytes\": 1024,\n    \"severity\": \"Ok\",\n    \"warnBytes\": 102400,\n    \"errorBytes\": 512000\n  }\n}"
      },
      {
       "name": "404 Not Found — anchor row not found",
       "status": "Not Found",
       "code": 404,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json"
        }
       ],
       "cookie": [],
       "body": "{\n  \"error\": \"No WorkOrderVisit row matches the supplied anchorIdentity in this tenant.\"\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "WhoAmI — caller self-description",
   "description": "GET-style identity endpoint every integrator hits at startup. Returns tenantId, tenantUserId, userId, language, timezone (IANA + DST-aware offset), serverTimeUtc, AND tenantHost / tenantSubDomain / tenantName for browser-URL construction without a separate Portal-URL config option. Used by the agent's --push-file to discover where to send the operator's browser. No role gate; valid token is the only check.",
   "item": [
    {
     "name": "Who am I?",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Authorization",
        "value": "Bearer {{apiKey}}"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/schema/whoami",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "schema",
        "whoami"
       ]
      },
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "description": "Self-describing endpoint. Returns:\n\n{\n  \"tenantId\": 1,\n  \"tenantUserId\": 4,\n  \"userId\": 2,\n  \"userName\": \"kbarrett\",\n  \"displayName\": \"Acme Corporation\",\n  \"roleFlag\": 134217215,\n  \"languageRegionId\": 69,\n  \"timeZoneId\": 24,\n  \"timeZoneIana\": \"America/Guatemala\",\n  \"timeZoneAbbrev\": \"CST\",\n  \"standardOffsetMinutes\": -360,\n  \"currentOffsetMinutes\": -360,\n  \"serverTimeUtc\": \"2026-04-28T07:58:40Z\",\n  \"serverTimeLocal\": \"2026-04-28T01:58:40\",\n  \"tenantHost\": \"localhost\",\n  \"tenantSubDomain\": \"acme\",\n  \"tenantName\": \"Acme Corporation\"\n}\n\nThe tenantHost / tenantSubDomain / tenantName fields (added 2026-04-28) let integrators construct browser-facing URLs (the dream wizard's hand-off URL, email [[PortalUrl]] tokens) without needing a separate Portal-URL config option. tenantHost is null when the tenant doesn't have a Host configured; integrators fall back to their own BaseUrl in that case."
     }
    }
   ]
  },
  {
   "name": "Maintenance",
   "item": [
    {
     "name": "Maintenance — purge soft-deleted (dry-run)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{globalKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"olderThanDays\": 30,\n  \"dryRun\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/maintenance/purge-soft-deleted",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "maintenance",
        "purge-soft-deleted"
       ]
      },
      "description": "Hard-deletes soft-deleted rows older than `olderThanDays`. AdminUser + global-tenant gated. Default `dryRun=true` — RUN PHASE 1 to see what WOULD purge before the real run. FK 547 errors → BlockedByLiveFK status row, doesn't fail the batch. Tenant + TenantUser tables auto-excluded (structural). Discovery walks sys.tables for `(ActiveEnd, TenantId)` carriers, topo-sorts via FK graph (Kahn's algorithm), purges in dependency order. Logged via IAuditLogger (Category=GlobalMaintenance, Action=PurgeSoftDeleted)."
     },
     "response": []
    },
    {
     "name": "Maintenance — purge soft-deleted (LIVE — destructive)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       },
       {
        "key": "x-api-key",
        "value": "{{globalKey}}",
        "type": "text"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"olderThanDays\": 30,\n  \"dryRun\": false,\n  \"confirmedHardDelete\": true,\n  \"tenantIdScope\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/maintenance/purge-soft-deleted",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "maintenance",
        "purge-soft-deleted"
       ]
      },
      "description": "**DESTRUCTIVE** — actually hard-deletes. Two-key gate: dryRun=false + confirmedHardDelete=true. Required Portal-side typed-name confirm. tenantIdScope optional (omit for global). Returns rows-purged-per-table summary + any FK-blocked rows."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Messaging",
   "description": "Messaging API v1 — send rich messages (WhatsApp/AMB/RCS) and read replies. Send + read need AdminIntegration; channel config needs AdminUser; concierge needs a global-admin (TenantId 0) key. The inbound webhook is unauthenticated (the provider calls it). The last two requests show the MCP/Copilot send + check-replies tools via /api/CopilotChat. See docs/public/Integrators/Messaging-API.md.",
   "item": [
    {
     "name": "Conversations - Erase one conversation",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channel\": \"WhatsApp\",\n  \"customerAddress\": \"17178080988\",\n  \"dryRun\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/conversations/purge",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "conversations",
        "purge"
       ]
      },
      "description": "AdminUser. Hard-erase ALL data for one conversation (MessagingEvent + delivery rows + durable turns + the routing pin, across both DBs). dryRun defaults TRUE -> preview counts { eventCount, deliveryCount, turnCount, pinCount }; send dryRun:false to actually erase. A non-global caller may only erase their own tenant; pass tenantId only as a global admin. Every call is audited (GlobalMaintenance / PurgeConversation). For bulk time-based cleanup install the Messaging Retention Action Pack instead. See docs/public/Integrators/Messaging-API.md section 3e."
     },
     "response": []
    },
    {
     "name": "Send — Text",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"text\",\n  \"text\": \"Your tech is 10 minutes out.\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Send plain text. Returns { ok, providerMessageId, token }. Keep the token to correlate the reply via List Messages."
     },
     "response": []
    },
    {
     "name": "Send — Buttons",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"buttons\",\n  \"body\": \"Confirm your appointment?\",\n  \"buttons\": [\n    { \"id\": \"yes\", \"title\": \"Yes\" },\n    { \"id\": \"no\", \"title\": \"No\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Interactive reply buttons. The correlation token is embedded in each button id, so the customer's tap comes back correlated."
     },
     "response": []
    },
    {
     "name": "Send — Interactive List",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"list\",\n  \"body\": \"Pick a service\",\n  \"listTitle\": \"Services\",\n  \"listItems\": [\n    { \"id\": \"ac-install\", \"title\": \"AC install\" },\n    { \"id\": \"ac-repair\", \"title\": \"AC repair\", \"description\": \"Diagnostic + fix\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Interactive list picker."
     },
     "response": []
    },
    {
     "name": "Send — CTA URL",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"cta-url\",\n  \"body\": \"Track your job\",\n  \"buttonText\": \"Open\",\n  \"buttonUrl\": \"https://example.com/track/123\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Tappable URL button."
     },
     "response": []
    },
    {
     "name": "Send — Image",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"image\",\n  \"mediaUrl\": \"https://example.com/photo.jpg\",\n  \"caption\": \"Before\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Send an image by URL."
     },
     "response": []
    },
    {
     "name": "Send — Location",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"location\",\n  \"latitude\": 40.27,\n  \"longitude\": -76.88,\n  \"locationName\": \"Job site\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Send a pin."
     },
     "response": []
    },
    {
     "name": "Send — Neutral (Ask, auto buttons/list)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"Ask\",\n    \"text\": \"Confirm your appointment?\",\n    \"options\": [\n      { \"id\": \"yes\", \"title\": \"Yes\" },\n      { \"id\": \"no\", \"title\": \"No\" }\n    ]\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "The channel-agnostic 'neutral' envelope. kind=Ask auto-maps to buttons (<=3 options) or a list (>3). Same envelope renders to RCS suggestions / AMB quick-reply when those channels are live. Set 'style':'List' to force a list."
     },
     "response": []
    },
    {
     "name": "Send — Neutral (Image)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"Media\",\n    \"mediaKind\": \"Image\",\n    \"mediaUrl\": \"https://example.com/photo.jpg\",\n    \"text\": \"Before\"\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Neutral media. mediaKind = Image | Video | Audio | Document | Sticker. 'text' is the caption."
     },
     "response": []
    },
    {
     "name": "Send — Document (rich kind)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"document\",\n  \"mediaUrl\": \"https://example.com/invoice.pdf\",\n  \"fileName\": \"invoice.pdf\",\n  \"caption\": \"Your invoice\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Per-kind rich media (also: video, audio, sticker). Document carries an optional fileName."
     },
     "response": []
    },
    {
     "name": "Send — Neutral (Carousel)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"Carousel\",\n    \"text\": \"Pick a plan\",\n    \"cards\": [\n      { \"title\": \"Basic\", \"text\": \"$10/mo\", \"mediaUrl\": \"https://example.com/basic.jpg\", \"options\": [ { \"id\": \"basic\", \"title\": \"Choose\" } ] },\n      { \"title\": \"Pro\", \"text\": \"$20/mo\", \"mediaUrl\": \"https://example.com/pro.jpg\", \"options\": [ { \"id\": \"pro\", \"title\": \"Choose\" } ] }\n    ]\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Neutral carousel -> WhatsApp media-carousel (and RCS CAROUSEL when live)."
     },
     "response": []
    },
    {
     "name": "Send → Template (WhatsApp, opens 24h window)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"channel\": \"WhatsApp\",\n  \"kind\": \"template\",\n  \"templateName\": \"appointment_reminder\",\n  \"templateLanguage\": \"en\",\n  \"templatePlaceholders\": [\n    \"Tuesday\",\n    \"2pm\"\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Template messages are the ONLY thing that sends OUTSIDE WhatsApp's 24h customer-service window (a session text outside it returns EC_NO_SESS). Placeholders fill the numbered slots in the approved template."
     },
     "response": []
    },
    {
     "name": "Send → Flow (WhatsApp Flow form)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"channel\": \"WhatsApp\",\n  \"kind\": \"flow\",\n  \"flowId\": \"1234567890\",\n  \"flowToken\": \"job-123\",\n  \"flowCta\": \"Start intake\",\n  \"flowBody\": \"Tap to fill the intake form.\",\n  \"flowScreen\": \"INTAKE\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Launch a native WhatsApp Flow (multi-screen form). flowToken is your correlation handle for the submitted data."
     },
     "response": []
    },
    {
     "name": "Send → Request Location (neutral)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"LocationRequest\",\n    \"text\": \"Share your location so we can find the nearest depot.\"\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Ask the contact to SHARE their location. The shared pin returns as an inbound LOCATION event (contentType=LOCATION, contentText=lat,lon) correlated to the conversation - the same coords an AI step can use for nearest-X / distance."
     },
     "response": []
    },
    {
     "name": "Send → Neutral (Contact card)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"Contact\",\n    \"contacts\": [\n      {\n        \"name\": \"Acme Dispatch\",\n        \"phone\": \"+17175551212\",\n        \"email\": \"dispatch@acme.example\"\n      }\n    ]\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Send a tappable contact card. Pairs with the identity-bridge email/phone fields when surfacing a person from a lookup."
     },
     "response": []
    },
    {
     "name": "Send → Neutral (Location pin)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"neutral\": {\n    \"kind\": \"Location\",\n    \"latitude\": 40.0379,\n    \"longitude\": -76.3055,\n    \"placeName\": \"Acme Depot\",\n    \"address\": \"100 Main St, Lancaster PA\"\n  }\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/send",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "send"
       ]
      },
      "description": "Drop a map pin (a location WE send, vs LocationRequest which asks the contact to share theirs)."
     },
     "response": []
    },
    {
     "name": "List Messages (check replies)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": \"Inbound\",\n  \"channel\": \"WhatsApp\",\n  \"correlationToken\": \"PASTE-TOKEN-FROM-SEND\",\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/inbound/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "inbound",
        "list"
       ]
      },
      "description": "Read the unified message log. Filter by correlationToken (to find the reply to a specific send) or by customerAddress + direction=Inbound. All filters optional."
     },
     "response": []
    },
    {
     "name": "Get Conversation (messages by handset)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"customerAddress\": \"17178080988\",\n  \"pageSize\": 100\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/inbound/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "inbound",
        "list"
       ]
      },
      "description": "AdminIntegration. Read a CONVERSATION -- the unified message log for one handset (omit direction to get both in + out; this is the same data the Message Center 'Export conversation' pulls). Filter by customerAddress (one handset) and/or correlationToken (one send's replies); all fields optional, newest-first, paged."
     },
     "response": []
    },
    {
     "name": "List Turns (one handset's threads)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channel\": \"WhatsApp\",\n  \"customerAddress\": \"17178080988\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/turns/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "turns",
        "list"
       ]
      },
      "description": "AdminIntegration. One handset's DURABLE re-tappable conversation TURNS (the thread graph behind the rich controls), newest-first. The per-handset companion to the tenant-wide /turns browse."
     },
     "response": []
    },
    {
     "name": "Browse Turns (tenant-wide threads)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channel\": \"WhatsApp\",\n  \"openOnly\": true,\n  \"search\": \"proof confirm\",\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/turns",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "turns"
       ]
      },
      "description": "AdminIntegration. Tenant-wide browse of the durable, re-tappable conversation TURNS across ALL handsets, newest-first, PINNED-FIRST. Every field optional (AND-combined): channel, customerAddress (one handset), awaiting (one parked step), openOnly (not-yet-closed), packInstanceId (one pack), pageNumber, pageSize, AND search (power search: every whitespace token must match the turn (label/awaiting/token) OR any message for the handset (text + markers like INTERACTIVE_LIST_REPLY/SEEN); contains, AND). Each result row also carries contactName (the resolved name behind the handset via the identity bridge; null if unbridged) + isPinned (the caller's conversation/turn pin). The cross-handset thread monitor + the integrator/agent turn feed."
     },
     "response": []
    },
    {
     "name": "Pin Turn / Conversation",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channel\": \"WhatsApp\",\n  \"customerAddress\": \"17178080988\",\n  \"messagingTurnId\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/turns/pin",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "turns",
        "pin"
       ]
      },
      "description": "AdminIntegration. Pin to the top of the caller's turns browse (per-user, idempotent). messagingTurnId 0 (or omitted) = pin the WHOLE conversation/handset; a turn id = pin just that turn. POST the same body to .../unpin to remove."
     },
     "response": []
    },
    {
     "name": "Unpin Turn / Conversation",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channel\": \"WhatsApp\",\n  \"customerAddress\": \"17178080988\",\n  \"messagingTurnId\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/turns/unpin",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "turns",
        "unpin"
       ]
      },
      "description": "AdminIntegration. Unpin a conversation (messagingTurnId 0) or a single turn for the caller. Idempotent."
     },
     "response": []
    },
    {
     "name": "Purge Turns (retention GC - dry run)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"olderThanDays\": 365,\n  \"dryRun\": true,\n  \"noiseOnly\": false,\n  \"closedOnly\": false,\n  \"packInstanceId\": 0\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/turns/purge",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "turns",
        "purge"
       ]
      },
      "description": "AdminUser. Count (dryRun=true, the DEFAULT - safe) or hard-delete (dryRun=false) this tenant's durable turns whose lastSeenUtc is older than olderThanDays (default 365). Optional scopes AND-combined: noiseOnly (registered but never resumed), closedOnly (explicitly ended), packInstanceId (one install). A resolve bumps lastSeenUtc so active threads never age out. The platform also runs an always-on daily sweep; this is the on-demand operator/agent trigger."
     },
     "response": []
    },
    {
     "name": "Simulate Inbound — Text",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"from\": \"17178080988\",\n  \"kind\": \"text\",\n  \"text\": \"Yes\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/simulate-inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "simulate-inbound"
       ]
      },
      "description": "Inject an inbound AS a handset, replayed through the real webhook pipeline. Great for testing without a live device."
     },
     "response": []
    },
    {
     "name": "Simulate Inbound — Button Reply",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"from\": \"17178080988\",\n  \"kind\": \"button\",\n  \"controlId\": \"PASTE-token~yes\",\n  \"controlTitle\": \"Yes\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/simulate-inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "simulate-inbound"
       ]
      },
      "description": "Simulate tapping a button. controlId is the token-embedded id (token~choiceId) of a button you sent, so it correlates."
     },
     "response": []
    },
    {
     "name": "Simulate Inbound — Image (media)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"from\": \"17178080988\",\n  \"kind\": \"image\",\n  \"mediaUrl\": \"{{baseUrl}}/mobile/icon-192.png\",\n  \"caption\": \"Test photo\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/simulate-inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "simulate-inbound"
       ]
      },
      "description": "Inject an inbound MEDIA message (image/video/audio/document/sticker) as a handset. The parser logs contentType=IMAGE with contentText = the media URL, exactly like a real provider MO - so the media-ingest (save-to-library) flow is testable without a live device."
     },
     "response": []
    },
    {
     "name": "Workflow loop - Start a workflow (tap __workflow)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"results\": [\n    {\n      \"messageId\": \"pm-wf-start-1\",\n      \"from\": \"17178080988\",\n      \"to\": \"15550009999\",\n      \"content\": {\n        \"type\": \"INTERACTIVE_BUTTON_REPLY\",\n        \"id\": \"__workflow:100004\",\n        \"title\": \"Travel and Arrival\"\n      }\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/webhooks/messaging/whatsapp/1/inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "webhooks",
        "messaging",
        "whatsapp",
        "1",
        "inbound"
       ]
      },
      "description": "THE W1 RUNNER OVER MESSAGING: start a published workflow as a conversation by replaying a tapped __workflow:{workflowId} global button through the per-tenant webhook (deterministic tenant resolution). The loop hydrates the pinned context, creates the shared WorkflowInstance, auto-flows Info steps, and serves the first interactive step to the handset. Watch the outbound events with List Messages. 100004 = the seeded Acme Travel & Arrival activity.",
      "auth": {
       "type": "noauth"
      }
     },
     "response": []
    },
    {
     "name": "Workflow loop - WORKFLOWS probe (live runs)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"results\": [\n    {\n      \"messageId\": \"pm-wf-probe-1\",\n      \"from\": \"17178080988\",\n      \"to\": \"15550009999\",\n      \"content\": {\n        \"type\": \"TEXT\",\n        \"text\": \"WORKFLOWS\"\n      }\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/webhooks/messaging/whatsapp/1/inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "webhooks",
        "messaging",
        "whatsapp",
        "1",
        "inbound"
       ]
      },
      "description": "The session-load probe: texting WORKFLOWS lists the handsets in-progress workflow runs as tap-to-resume __wfrun:{instanceId} buttons (or a friendly none-in-progress reply).",
      "auth": {
       "type": "noauth"
      }
     },
     "response": []
    },
    {
     "name": "Workflow loop - Answer the current step (typed)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"from\": \"17178080988\",\n  \"kind\": \"text\",\n  \"text\": \"2\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/simulate-inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "simulate-inbound"
       ]
      },
      "description": "Answer the served step by typing - the reply parser resolves an exact option label, a 1-based number (2), or a unique partial word (cool -> No cooling), and validates text/number rules exactly like the web runner. Invalid input re-serves the step with the error; valid input saves the SAME StepInstance rows the web writes and advances."
     },
     "response": []
    },
    {
     "name": "Workflow loop - Resume a live run (tap __wfrun)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"results\": [\n    {\n      \"messageId\": \"pm-wf-resume-1\",\n      \"from\": \"17178080988\",\n      \"to\": \"15550009999\",\n      \"content\": {\n        \"type\": \"INTERACTIVE_BUTTON_REPLY\",\n        \"id\": \"__wfrun:17\",\n        \"title\": \"Resume\"\n      }\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/webhooks/messaging/whatsapp/1/inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "webhooks",
        "messaging",
        "whatsapp",
        "1",
        "inbound"
       ]
      },
      "description": "Resume a specific in-progress run by replaying its __wfrun:{workflowInstanceId} button (what the WORKFLOWS probe sends). The loop reloads the shared instance and re-serves the current step.",
      "auth": {
       "type": "noauth"
      }
     },
     "response": []
    },
    {
     "name": "Media — Save inbound to library (ingest)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"mediaUrl\": \"<the inbound event's contentText>\",\n  \"channel\": \"WhatsApp\",\n  \"title\": \"Dataplate photo - WO-5002\",\n  \"customerAddress\": \"17178080988\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/media/ingest",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "media",
        "ingest"
       ]
      },
      "description": "Download an INBOUND media file (the provider URL an IMAGE/VIDEO/DOCUMENT event logged as its contentText) and store it in the tenant's binary-asset library. The channel's API key authenticates the download ONLY for provider-owned hosts. Optional storageShortcutId routes the destination (omit = tenant default). Returns assetKey + a viewable url; appends a synthetic ASSET event to the conversation when customerAddress is supplied. On-demand by design - inbound media is never auto-saved. AdminIntegration."
     },
     "response": []
    },
    {
     "name": "Channels — List",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/channels/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "channels",
        "list"
       ]
      },
      "description": "AdminUser. Secure list — credentials are never returned, only a hasCreds flag."
     },
     "response": []
    },
    {
     "name": "Channels — Save",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantId\": 1,\n  \"channelType\": \"WhatsApp\",\n  \"sender\": \"15551234567\",\n  \"displayName\": \"Acme Support\",\n  \"baseUrl\": \"https://YOUR-SUB.api-us.infobip.com\",\n  \"apiKey\": \"YOUR-INFOBIP-KEY\",\n  \"isDefault\": true\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/channels/save",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "channels",
        "save"
       ]
      },
      "description": "AdminUser. Create/update. On update send apiKey:null to KEEP the existing secret (write-only). isDefault:true demotes other defaults for the type."
     },
     "response": []
    },
    {
     "name": "Channels — Delete",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"tenantMessagingChannelId\": 123,\n  \"tenantId\": 1\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/channels/delete",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "channels",
        "delete"
       ]
      },
      "description": "AdminUser. Soft-delete — the tenant reverts to inheriting the platform default."
     },
     "response": []
    },
    {
     "name": "Concierge — Route to Tenant",
     "request": {
      "method": "POST",
      "auth": {
       "type": "bearer",
       "bearer": [
        {
         "key": "token",
         "value": "{{globalKey}}",
         "type": "string"
        }
       ]
      },
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"customerAddress\": \"17178080988\",\n  \"channel\": \"WhatsApp\",\n  \"tenantId\": 1,\n  \"sendHello\": true,\n  \"helloText\": \"Hi! You're connected to Acme.\"\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/concierge/route",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "concierge",
        "route"
       ]
      },
      "description": "Global admin (TenantId 0) only. Pins an unknown conversation to a tenant; future messages from that handset follow."
     },
     "response": []
    },
    {
     "name": "Inbound Webhook (firehose, UNAUTH)",
     "request": {
      "auth": {
       "type": "noauth"
      },
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"results\": [\n    {\n      \"messageId\": \"demo-1\",\n      \"from\": \"17178080988\",\n      \"to\": \"447860099299\",\n      \"content\": { \"type\": \"TEXT\", \"text\": \"hello\" }\n    }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/webhooks/messaging/whatsapp/inbound",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "webhooks",
        "messaging",
        "whatsapp",
        "inbound"
       ]
      },
      "description": "The provider calls this (no auth). Firehose form (no tenant segment) resolves the tenant per-event. Returns { saved, failed }. Shown for reference / local testing."
     },
     "response": []
    },
    {
     "name": "MCP/Copilot — Send via tool",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"Send a WhatsApp text to 17178080988 saying the tech is 10 minutes out.\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/CopilotChat",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "CopilotChat"
       ]
      },
      "description": "Agent path: the default Copilot surface exposes the send_message MCP tool. Secured by the same bearer/API key. The LLM picks send_message and the response carries the action result + correlation token."
     },
     "response": []
    },
    {
     "name": "MCP/Copilot — Check replies via tool",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"messages\": [\n    { \"role\": \"user\", \"content\": \"Did 17178080988 reply yet? Show the latest inbound messages.\" }\n  ]\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/CopilotChat",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "CopilotChat"
       ]
      },
      "description": "The list_messages MCP tool. The LLM reads the unified log and summarizes the replies."
     },
     "response": []
    },
    {
     "name": "Event Subscriptions (push-to-interface)",
     "description": "Register a callback URL and we POST matching side-band events (replies / changed answers / receipts) to it - the inverse of polling the log. correlationToken = your receipt; eventId = idempotency/dedup key. Every tenant starts with a self://loopback self-test subscription. AdminIntegration scope (the loopback receiver is unauth).",
     "item": [
      {
       "name": "Register Subscription (your URL)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"channel\": \"WhatsApp\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"prod inbound replies\",\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "Register/update a push subscription. Omit channel/eventTypes (or null) = all. Returns { subscriptionId }. Duplicate callbackUrl for the tenant = 409."
       },
       "response": [],
       "event": [
        {
         "listen": "test",
         "script": {
          "type": "text/javascript",
          "exec": [
           "var j = pm.response.json();",
           "if (j && j.subscriptionId) { pm.collectionVariables.set('subscriptionId', j.subscriptionId); }",
           "pm.test('got subscriptionId', function(){ pm.expect(j.subscriptionId).to.be.a('number'); });"
          ]
         }
        }
       ]
      },
      {
       "name": "Register Subscription (filtered: sender + control type)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"only button taps from one handset\",\n  \"filterJson\": {\n    \"logic\": \"AND\",\n    \"rules\": [\n      {\n        \"field\": \"customerAddress\",\n        \"op\": \"equals\",\n        \"value\": \"17178080988\"\n      },\n      {\n        \"field\": \"contentType\",\n        \"op\": \"equals\",\n        \"value\": \"INTERACTIVE_BUTTON_REPLY\"\n      }\n    ]\n  },\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "filterJson is a FilterGroup rule tree (same shape as Views/Reports filters) on top of channel + eventTypes. Filterable fields: customerAddress (sender/number), contentType (control type: TEXT / INTERACTIVE_BUTTON_REPLY / LOCATION / ...), channel, contentText (keyword via op=contains), correlationToken. logic = AND | OR. Omit for no extra filter."
       },
       "response": []
      },
      {
       "name": "Register Subscription (loopback self-test)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"self://loopback\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"built-in self-test\"\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "self://loopback resolves to THIS tenant's own receiver. Texting a reply then shows a SubscriptionLoopback event in the log - proof the push pipeline works end-to-end with no external endpoint. (Seeded by default for demo tenants.)"
       },
       "response": []
      },
      {
       "name": "Update Subscription (change filter)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"subscriptionId\": \"{{subscriptionId}}\",\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"channel\": \"\",\n  \"eventTypes\": \"Inbound,DeliveryReport,SeenReceipt\",\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "Pass subscriptionId to update. Here: widen to ALL channels (blank) and subscribe to receipts too (CSV)."
       },
       "response": []
      },
      {
       "name": "List Subscriptions",
       "request": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "List this tenant's active subscriptions: { results: [ { messagingSubscriptionId, callbackUrl, channel, eventTypes, description, isEnabled, createdDate } ] }."
       },
       "response": []
      },
      {
       "name": "List Deliveries (delivery log)",
       "request": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions/{{subscriptionId}}/deliveries?pageNumber=1&pageSize=25",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions",
          "{{subscriptionId}}",
          "deliveries"
         ],
         "query": [
          {
           "key": "pageNumber",
           "value": "1"
          },
          {
           "key": "pageSize",
           "value": "25"
          }
         ]
        },
        "description": "Paged delivery log for one subscription (newest first). Each row carries the MessagingDelivery status (Pending/Delivered/Failed/DeadLettered), attemptCount, nextAttemptUtc, lastError, PLUS the MessagingEvent it delivered (eventType, eventChannel, eventCorrelationToken, eventContentText, ...). Powers the Portal Ops -> Integrations delivery-log viewer."
       },
       "response": []
      },
      {
       "name": "Delete Subscription",
       "request": {
        "method": "DELETE",
        "header": [],
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions/{{subscriptionId}}",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions",
          "{{subscriptionId}}"
         ]
        },
        "description": "Soft-delete a subscription. Returns { deleted: true }."
       },
       "response": []
      },
      {
       "name": "Loopback receiver (UNAUTH - the payload WE POST to you)",
       "request": {
        "method": "POST",
        "header": [],
        "auth": {
         "type": "noauth"
        },
        "body": {
         "mode": "raw",
         "raw": "{\n  \"subscriptionId\": 7,\n  \"eventId\": 42,\n  \"tenantId\": 1,\n  \"channel\": \"WhatsApp\",\n  \"eventType\": \"Inbound\",\n  \"correlationToken\": \"k0a1b2c3d4e5\",\n  \"customerAddress\": \"17178080988\",\n  \"contentType\": \"TEXT\",\n  \"contentText\": \"Yes\",\n  \"receivedAt\": \"2026-06-02T10:00:00Z\",\n  \"statusName\": null\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions/loopback",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions",
          "loopback"
         ]
        },
        "description": "The shape of the payload WE deliver to your callbackUrl (this is the built-in self://loopback target; your endpoint receives the identical body). Return any 2xx to acknowledge; de-dupe on eventId; correlate on correlationToken."
       },
       "response": []
      },
      {
       "name": "Subscribe to a send's reply (token filter)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"push me the answer to this one question\",\n  \"filterJson\": {\n    \"rules\": [\n      {\n        \"field\": \"correlationToken\",\n        \"op\": \"equals\",\n        \"value\": \"{{lastToken}}\"\n      }\n    ]\n  },\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "The 'smart subscription sender' pattern: after POST /messaging/send returns a token (saved to {{lastToken}} by the Conversation walkthrough), register a subscription filtered to that correlationToken. We then PUSH only the reply to THAT send to your callbackUrl - send a question, get notified of its answer, fully programmatic. Delete the subscription once you've got the answer (it's a one-question interest)."
       },
       "response": []
      },
      {
       "name": "Register Subscription (filtered: keyword in message)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"only messages mentioning 'refund'\",\n  \"filterJson\": {\n    \"rules\": [\n      {\n        \"field\": \"contentText\",\n        \"op\": \"contains\",\n        \"value\": \"refund\"\n      }\n    ]\n  },\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "Keyword filter: push only inbound whose text contains 'refund' (case-insensitive). field=contentText, op=contains."
       },
       "response": []
      },
      {
       "name": "Register Subscription (filtered: buttons OR locations)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"callbackUrl\": \"{{callbackUrl}}\",\n  \"eventTypes\": \"Inbound\",\n  \"description\": \"interactive taps or shared locations\",\n  \"filterJson\": {\n    \"logic\": \"OR\",\n    \"rules\": [\n      {\n        \"field\": \"contentType\",\n        \"op\": \"equals\",\n        \"value\": \"INTERACTIVE_BUTTON_REPLY\"\n      },\n      {\n        \"field\": \"contentType\",\n        \"op\": \"equals\",\n        \"value\": \"LOCATION\"\n      }\n    ]\n  },\n  \"isEnabled\": true\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/subscriptions",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "subscriptions"
         ]
        },
        "description": "OR logic: push button/list taps OR shared locations - skip everything else. Shows logic=OR across two contentType rules."
       },
       "response": []
      }
     ]
    },
    {
     "name": "Conversation walkthrough (send → correlate → resume)",
     "description": "A runnable trace of the conversational model (see the Conversational-Workflows explainer): send an interactive control, the reply comes back correlated by token, you branch/resume on your side by matching that token. Run top-to-bottom; step 1 saves the token to {{lastToken}} for the rest.",
     "item": [
      {
       "name": "1. Send buttons (captures token → {{lastToken}})",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"buttons\",\n  \"body\": \"Confirm your 2pm appointment?\",\n  \"buttons\": [\n    {\n      \"id\": \"yes\",\n      \"title\": \"Yes\"\n    },\n    {\n      \"id\": \"no\",\n      \"title\": \"No\"\n    },\n    {\n      \"id\": \"reschedule\",\n      \"title\": \"Reschedule\"\n    }\n  ]\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/send",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "send"
         ]
        },
        "description": "Send an interactive control. The returned token is embedded in each button id (token~choiceId) so the reply routes back to THIS thread. Saved to {{lastToken}} by the test script."
       },
       "response": [],
       "event": [
        {
         "listen": "test",
         "script": {
          "type": "text/javascript",
          "exec": [
           "var j = pm.response.json();",
           "if (j && j.token) { pm.collectionVariables.set('lastToken', j.token); }",
           "pm.test('got token', function(){ pm.expect(j.token).to.be.a('string'); });"
          ]
         }
        }
       ]
      },
      {
       "name": "2. Simulate the reply (tap Yes)",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"from\": \"17178080988\",\n  \"kind\": \"button\",\n  \"controlId\": \"{{lastToken}}~yes\",\n  \"controlTitle\": \"Yes\"\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/simulate-inbound",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "simulate-inbound"
         ]
        },
        "description": "Impersonate the handset tapping Yes. controlId = {{lastToken}}~yes carries the token home, exactly as a real WhatsApp interactive reply would. (In production the contact taps; this just drives it without a live handset.)"
       },
       "response": []
      },
      {
       "name": "3. Read the reply by token",
       "request": {
        "method": "POST",
        "header": [],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"correlationToken\": \"{{lastToken}}\",\n  \"pageSize\": 50\n}",
         "options": {
          "raw": {
           "language": "json"
          }
         }
        },
        "url": {
         "raw": "{{baseUrl}}/api/v1/messaging/inbound/list",
         "host": [
          "{{baseUrl}}"
         ],
         "path": [
          "api",
          "v1",
          "messaging",
          "inbound",
          "list"
         ]
        },
        "description": "Pull every event sharing this token - your send AND the correlated reply. This is the POLL side; an Event Subscription would PUSH the same reply to your URL."
       },
       "response": []
      }
     ]
    },
    {
     "name": "Agent loop - Converse (send + wire inbox)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"buttons\",\n  \"body\": \"Confirm your 2pm?\",\n  \"buttons\": [\n    { \"id\": \"yes\", \"title\": \"Yes\" },\n    { \"id\": \"no\", \"title\": \"No\" }\n  ],\n  \"replyMode\": \"capture\",\n  \"expiresMinutes\": 60\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/converse",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "converse"
       ]
      },
      "description": "AGENT LOOP. Send a rich message AND wire a durable, ordered return inbox in one call. Returns { token, subscriptionId }. token = conversation handle (reuse to continue); subscriptionId = inbox handle (drain/end). Saves both to collection vars. Then run Drain to read replies in order, End to finish."
     },
     "response": [],
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "var j = pm.response.json();",
         "if (j && j.subscriptionId) { pm.collectionVariables.set('subscriptionId', j.subscriptionId); }",
         "if (j && j.token) { pm.collectionVariables.set('threadToken', j.token); }"
        ]
       }
      }
     ]
    },
    {
     "name": "Agent loop - Ask (send + bounded wait)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"to\": \"17178080988\",\n  \"kind\": \"buttons\",\n  \"body\": \"Approve PO #4471?\",\n  \"buttons\": [\n    { \"id\": \"approve\", \"title\": \"Approve\" },\n    { \"id\": \"reject\", \"title\": \"Reject\" }\n  ],\n  \"waitSeconds\": 30\n}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/ask",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "ask"
       ]
      },
      "description": "AGENT LOOP. Converse AND block briefly for the answer (confirm / approve / accept / sign-in proof). Returns { gotReply, replies, subscriptionId }. gotReply=false = timed out; the thread stays open, poll the subscriptionId later. Blocks only this request, never the platform. Saves subscriptionId."
     },
     "response": [],
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "var j = pm.response.json();",
         "if (j && j.subscriptionId) { pm.collectionVariables.set('subscriptionId', j.subscriptionId); }",
         "if (j && j.token) { pm.collectionVariables.set('threadToken', j.token); }"
        ]
       }
      }
     ]
    },
    {
     "name": "Agent loop - Threads (list open)",
     "request": {
      "method": "GET",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/threads",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "threads"
       ]
      },
      "description": "AGENT LOOP. List the open conversation threads for this tenant: { subscriptionId, threadToken, recipient, replyMode, expiresUtc }. Resume an existing thread (reuse threadToken) or tidy a stale one (End) before opening a duplicate. Add ?to=17178080988 to filter."
     },
     "response": []
    },
    {
     "name": "Agent loop - Drain (poll replies in order)",
     "request": {
      "method": "POST",
      "header": [],
      "body": {
       "mode": "raw",
       "raw": "{}",
       "options": {
        "raw": {
         "language": "json"
        }
       }
      },
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/subscriptions/{{subscriptionId}}/drain?take=10",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "subscriptions",
        "{{subscriptionId}}",
        "drain"
       ],
       "query": [
        {
         "key": "take",
         "value": "10"
        }
       ]
      },
      "description": "AGENT LOOP poll. Claim the oldest-N queued replies for this inbox IN ORDER and mark them delivered (never twice). Empty = nothing yet, poll again. Uses {{subscriptionId}} saved by Converse/Ask."
     },
     "response": []
    },
    {
     "name": "Agent loop - End (delete + purge)",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/messaging/subscriptions/{{subscriptionId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "messaging",
        "subscriptions",
        "{{subscriptionId}}"
       ]
      },
      "description": "AGENT LOOP. End the thread: soft-delete the subscription AND purge its undelivered queue. Always call when the conversation is done. Uses {{subscriptionId}}."
     },
     "response": []
    }
   ]
  },
  {
   "name": "Action Packs",
   "description": "**Action Packs - tenant-installable, trigger-driven apps** (`/api/v1/actions/*`). A pack is DEFINED in the global catalog (`TenantId=0`), a tenant INSTALLS it (`ActionPackInstance` + SettingsJson), and it runs on one of 10 trigger types (OnDemand / Timer / Webhook live; Messaging is the Phase B payoff). Every run is logged to the run log (Action Center).\n\n**Auth:** definitions + installs gate **BuilderAdmin**; run / simulate / runs gate **AdminIntegration**; the webhook is unauthenticated (URL obscurity). Set `{{apiKey}}` to a tenant key with those roles. Identical contract on API.WebApi and API.Functions.\n\n**Quick start:** `installs/list` -> note an `actionPackInstanceId` -> set the `{{actionPackInstanceId}}` variable -> Run pack -> read it back in `runs/list` + `runs/{id}`.",
   "item": [
    {
     "name": "Run pack (OnDemand trigger)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/{{actionPackInstanceId}}/run",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "{{actionPackInstanceId}}",
        "run"
       ]
      },
      "description": "**Trigger #1 - On Demand.** Runs an installed pack now and returns the run id + outcome. Body: `inputText` (free text) and/or `inputJson` (structured). Response: `{ ok, actionPackRunId, statusId, output, result, error }` (statusId 2 = Succeeded). Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"inputText\": \"hello action packs\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Simulate a trigger",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/{{actionPackInstanceId}}/simulate",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "{{actionPackInstanceId}}",
        "simulate"
       ]
      },
      "description": "Run an installed pack now under a chosen `triggerTypeId` (+ optional `channel` / `customerAddress` to simulate a messaging run) without a live trigger. Mirrors Message Center's simulate. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"triggerTypeId\": 1,\n  \"inputText\": \"simulated input\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Webhook trigger (UNAUTH)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/webhook/{{tenantId}}/{{actionPackInstanceId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "webhook",
        "{{tenantId}}",
        "{{actionPackInstanceId}}"
       ]
      },
      "description": "**Trigger #6 - Webhook.** Unauthenticated (URL obscurity, like the messaging webhook). Any JSON body becomes the run's InputJson. Always returns 200 so a provider's retry timer never fires: `{ received, ran, actionPackRunId, statusId }`. Point a provider's webhook at this URL per installed pack.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"event\": \"ping\",\n  \"n\": 42\n}"
      },
      "auth": {
       "type": "noauth"
      }
     },
     "response": []
    },
    {
     "name": "Packs - list definitions",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/packs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "packs",
        "list"
       ]
      },
      "description": "List the tenant's own Action Pack definitions (TenantId-scoped). The global catalog is browsed via `packs/browse`. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}"
      }
     },
     "response": []
    },
    {
     "name": "Packs - browse catalog (not-yet-installed)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/packs/browse",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "packs",
        "browse"
       ]
      },
      "description": "Browse the global catalog (`TenantId=0`) for packs the tenant has not installed yet - the install picker. `channelScopeFlag` 0 = all; or intersect a bitmask (Portal=1, Messaging=2, Mobile=4, Api=8). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"channelScopeFlag\": 0\n}"
      }
     },
     "response": []
    },
    {
     "name": "Packs - detail (incl. DefinitionJson)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/packs/{{actionPackId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "packs",
        "{{actionPackId}}"
       ]
      },
      "description": "A definition detail incl. `definitionJson` (keywords + configSchema + recipe). Resolves catalog (`TenantId=0`) defs too, so a tenant can read a catalog pack's configSchema before installing. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Packs - create / update definition",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/packs",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "packs"
       ]
      },
      "description": "Create (`actionPackId: 0`) or update a definition. `handlerKey` = a coded handler (e.g. `Echo`), or null for a no-code `definitionJson.recipe`. `channelScopeFlag` bitmask (Portal=1, Messaging=2, Mobile=4, Api=8). 409 on duplicate `(internalName, version)`. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionPackId\": 0,\n  \"internalName\": \"MyPack\",\n  \"displayName\": \"My Pack\",\n  \"description\": \"What it does.\",\n  \"version\": \"1.0.0\",\n  \"actionPackTriggerTypeId\": 1,\n  \"channelScopeFlag\": 8,\n  \"handlerKey\": \"Echo\",\n  \"definitionJson\": \"{\\\"keywords\\\": [\\\"echo\\\"], \\\"configSchema\\\": [{\\\"key\\\": \\\"note\\\", \\\"label\\\": \\\"Note\\\", \\\"dataType\\\": \\\"text\\\", \\\"required\\\": false, \\\"help\\\": \\\"Optional note included in the echo.\\\"}]}\",\n  \"requiresIdentity\": false,\n  \"requiresConfiguration\": false,\n  \"isSystem\": false\n}"
      }
     },
     "response": []
    },
    {
     "name": "Packs - delete definition",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/packs/{{actionPackId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "packs",
        "{{actionPackId}}"
       ]
      },
      "description": "Soft-delete a definition. **409 Conflict** when live installs reference it - uninstall them first. Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "Installs - list",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs",
        "list"
       ]
      },
      "description": "List the tenant's installed packs (each carries SettingsJson, IsEnabled, trigger type, schedule). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"pageNumber\": 1,\n  \"pageSize\": 50\n}"
      }
     },
     "response": []
    },
    {
     "name": "Installs - install / update",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs"
       ]
      },
      "description": "Install a catalog/tenant pack (`actionPackInstanceId: 0`) or update an existing install. `settingsJson` fills the pack's configSchema. `frequencyMinutes` drives the Timer trigger. 409 on duplicate instance name. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionPackInstanceId\": 0,\n  \"actionPackId\": 1,\n  \"instanceName\": \"My Echo\",\n  \"channelScopeFlag\": 8,\n  \"isEnabled\": true,\n  \"settingsJson\": \"{\\\"note\\\":\\\"hi\\\"}\",\n  \"frequencyMinutes\": 0\n}"
      }
     },
     "response": []
    },
    {
     "name": "Installs - detail",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs/{{actionPackInstanceId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs",
        "{{actionPackInstanceId}}"
       ]
      },
      "description": "Full install detail incl. SettingsJson + the pack DefinitionJson (the config dialog binds to both). Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Installs - uninstall",
     "request": {
      "method": "DELETE",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs/{{actionPackInstanceId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs",
        "{{actionPackInstanceId}}"
       ]
      },
      "description": "Soft-delete (uninstall) an install. Run history is preserved. Role: **BuilderAdmin**."
     },
     "response": []
    },
    {
     "name": "Installs - enable / disable (kill switch)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/installs/{{actionPackInstanceId}}/enabled",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "installs",
        "{{actionPackInstanceId}}",
        "enabled"
       ]
      },
      "description": "Flip an install's enabled kill switch. Disabled installs are skipped by the auto-triggers (Timer / Webhook / Messaging) but can still be run on demand. Role: **BuilderAdmin**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"isEnabled\": false\n}"
      }
     },
     "response": []
    },
    {
     "name": "Runs - log list (filterable)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/runs/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "runs",
        "list"
       ]
      },
      "description": "The run log, newest first. Optional filters: `actionPackInstanceId`, `actionPackRunStatusId`, `actionPackTriggerTypeId`, `customerAddress`, `correlationToken`, `sinceUtc`. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"pageNumber\": 1,\n  \"pageSize\": 25\n}"
      }
     },
     "response": []
    },
    {
     "name": "Runs - detail + steps",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/runs/{{actionPackRunId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "runs",
        "{{actionPackRunId}}"
       ]
      },
      "description": "A run header + its ordered steps (StepKind, Label, in/out JSON, status, duration) - the drill view behind Action Center. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Subscriptions - list",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/list",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "list"
       ]
      },
      "description": "List ActionPackSubscription rows for this tenant. Body { actionPackInstanceId } scopes to one install; 0/omitted returns every subscription across every install. Used by the operator UI + an LLM-as-integrator discovering what fan-out is already wired. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{ \"actionPackInstanceId\": 0 }"
      }
     },
     "response": []
    },
    {
     "name": "Subscriptions - save (filtered Webhook on WorkflowCompleted)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/save",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "save"
       ]
      },
      "description": "Create or update a subscription. id=0 inserts; non-zero updates that row. triggerTypeId is 11 (WorkflowStarted) / 2 (WorkflowStepCompleted) / 3 (WorkflowCompleted). FilterJson is an optional FilterGroup over the flattened event row dict - null = match every event of this trigger. The body below subscribes the demo WebhookOutbound install to completions of one named workflow (the Kirk recipe). Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionPackSubscriptionId\": 0,\n  \"actionPackInstanceId\": {{actionPackInstanceId}},\n  \"actionPackTriggerTypeId\": 3,\n  \"internalName\": \"TravelAndArrival_Completed\",\n  \"filterJson\": \"{\\\"logic\\\":\\\"AND\\\",\\\"rules\\\":[{\\\"field\\\":\\\"workflowInternalName\\\",\\\"op\\\":\\\"eq\\\",\\\"value\\\":\\\"Acme_TravelAndArrival\\\"}]}\",\n  \"isEnabled\": true\n}"
      }
     },
     "response": []
    },
    {
     "name": "Subscriptions - detail",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/{{actionPackSubscriptionId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "{{actionPackSubscriptionId}}"
       ]
      },
      "description": "Read one subscription row + its FilterJson. The author UI uses this to load the row into the rule editor. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     },
     "response": []
    },
    {
     "name": "Subscriptions - delete",
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/{{actionPackSubscriptionId}}",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "{{actionPackSubscriptionId}}"
       ]
      },
      "description": "Soft-delete one subscription. The dispatcher's next read will skip it. Role: **AdminIntegration**."
     },
     "response": []
    },
    {
     "name": "Subscriptions - filter-fields (the discovery surface)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/filter-fields",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "filter-fields"
       ]
      },
      "description": "Return the EXACT set of filter targets available for a given trigger - common identity keys (workflowInternalName / userId / tenantUserId / correlationToken / ...) + trigger-specific keys (launchSource / path / valueJson / stepInternalName / completedUtc) + workflow-specific keys when workflowPublishedId is supplied (every authored step's InternalName for trigger 2; every authored OutputParam name for trigger 3). The author UI walks this list to build a no-guess filter. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionPackTriggerTypeId\": 3,\n  \"workflowPublishedId\": null\n}"
      }
     },
     "response": []
    },
    {
     "name": "Subscriptions - sample-event (preview the wire body)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/actions/subscriptions/sample-event",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "actions",
        "subscriptions",
        "sample-event"
       ]
      },
      "description": "Return the EXACT JSON shape the dispatcher would emit as the wire body for this trigger + workflow. The same body an integrator's endpoint receives. Authors use this to verify their FilterJson rule will fire on the right shape BEFORE turning on the subscription. Role: **AdminIntegration**.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"actionPackTriggerTypeId\": 3,\n  \"workflowPublishedId\": {{workflowPublishedId}}\n}"
      }
     },
     "response": []
    }
   ]
  },
  {
   "name": "Address Intelligence",
   "item": [
    {
     "name": "Standardize (Level 1)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/addresses/standardize",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "addresses",
        "standardize"
       ]
      },
      "description": "Level 1: parse + canonicalize a free-text US address (USPS) and return a stable `addressHash` (the dedup/cache key). Pure, no I/O. Any authenticated user / tenant API key.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"address\": \"1857 william penn way lancaster pa 17601\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Enrich (Level 1 + 2)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/addresses/enrich",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "addresses",
        "enrich"
       ]
      },
      "description": "Level 1 + 2: standardize then add CERTAIN ZIP reference data (`zipInfo`: county, FIPS, ZIP centroid, timezone, metro, area code) from the platform's own US ZIP DB. No external call.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"address\": \"1857 william penn way lancaster pa 17601\"\n}"
      }
     },
     "response": []
    },
    {
     "name": "Resolve (Level 1 + 2 + 3, geocode)",
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{baseUrl}}/api/v1/addresses/resolve",
       "host": [
        "{{baseUrl}}"
       ],
       "path": [
        "api",
        "v1",
        "addresses",
        "resolve"
       ]
      },
      "description": "Level 1 + 2 + 3: standardize, ZIP-enrich, then resolve EXACT lat/long via Azure Maps (cached platform-wide by `addressHash`). `level` caps the work (1/2/3, default 3); `forceFresh:true` bypasses the cache.",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"address\": \"1857 william penn way lancaster pa 17601\",\n  \"level\": 3,\n  \"forceFresh\": false\n}"
      }
     },
     "response": []
    }
   ]
  }
 ]
}
