{"info":{"_postman_id":"4a6d35db-c5ae-4b22-984f-d31c2c43b002","name":"LawVo - Inbound API","description":"<html><head></head><body><h1 id=\"lawvo-inbound-partner-api\">LawVo inbound partner API</h1>\n<p>This collection covers inbound partner endpoints:</p>\n<ul>\n<li><strong>POST</strong> <code>/generate-sso-link</code> - one-time member login URL</li>\n<li><strong>GET</strong> <code>/inbound/document-templates</code> - paginated document template catalog (Basic Information metadata)</li>\n</ul>\n<p>Partner guides:</p>\n<ul>\n<li><a href=\"../inbound-generate-sso-link.md\">inbound-generate-sso-link.md</a></li>\n<li><a href=\"../inbound-landing-context.md\">inbound-landing-context.md</a></li>\n<li><a href=\"../inbound-document-templates.md\">inbound-document-templates.md</a></li>\n</ul>\n<h2 id=\"setup\">Setup</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Variable</th>\n<th>Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>baseUrl</code></td>\n<td>UAT: <code>https://stagingapi.lawvo.com</code> · Live: <code>https://api.lawvo.com</code> (no trailing slash)</td>\n</tr>\n<tr>\n<td><code>PARTNER_API_KEY</code></td>\n<td>Inbound API key from LawVo</td>\n</tr>\n<tr>\n<td><code>PARTNER_HMAC_SECRET</code></td>\n<td>Paired HMAC secret</td>\n</tr>\n<tr>\n<td><code>documentTemplatesCategoryId</code></td>\n<td>Category id for filter examples (folder 6)</td>\n</tr>\n<tr>\n<td><code>documentTemplatesTagId</code></td>\n<td>Tag id for filter examples (folder 6)</td>\n</tr>\n</tbody>\n</table>\n</div><p><strong>POST</strong> requests sign the exact raw JSON body. <strong>GET</strong> <code>/inbound/document-templates</code> signs <code>GET\\n/inbound/document-templates\\n&lt;canonical query&gt;</code> where the query is sorted by parameter name.</p>\n<p>Every authenticated request requires <strong>both</strong> <code>X-API-Key</code> (or Bearer) <strong>and</strong> <code>X-Webhook-Signature: sha256=&lt;hex&gt;</code>.</p>\n<h2 id=\"supported-origin-slugs\">Supported <code>origin</code> slugs</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>origin</code> slug</th>\n<th>LawVo section</th>\n<th>App path after login</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>dashboard</code></td>\n<td>Dashboard</td>\n<td><code>/dashboard</code></td>\n</tr>\n<tr>\n<td><code>legal_ai</code></td>\n<td>LawVo AI</td>\n<td><code>/legal-ai</code></td>\n</tr>\n<tr>\n<td><code>knowledge_center</code></td>\n<td>Knowledge Center</td>\n<td><code>/subgroups</code></td>\n</tr>\n<tr>\n<td><code>document_hub</code></td>\n<td>Document Hub</td>\n<td><code>/document-center/?key=3</code></td>\n</tr>\n<tr>\n<td><code>document_library</code></td>\n<td>Document Library</td>\n<td><code>/document-library</code></td>\n</tr>\n<tr>\n<td><code>wills</code></td>\n<td>Wills</td>\n<td><code>/wills</code></td>\n</tr>\n<tr>\n<td><code>legal_support</code></td>\n<td>24/7 Legal Help</td>\n<td><code>/legal-support</code></td>\n</tr>\n<tr>\n<td><code>settings</code></td>\n<td>Settings</td>\n<td><code>/settings</code></td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"supported-landingcontextservicecategory-slugs\">Supported <code>landingContext.serviceCategory</code> slugs</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th><code>serviceCategory</code> slug</th>\n<th>Document Hub category</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>will</code></td>\n<td>Wills</td>\n</tr>\n<tr>\n<td><code>wills</code></td>\n<td>Wills</td>\n</tr>\n<tr>\n<td><code>real_estate</code></td>\n<td>Real Estate</td>\n</tr>\n<tr>\n<td><code>realestate</code></td>\n<td>Real Estate</td>\n</tr>\n<tr>\n<td><code>family</code></td>\n<td>Family</td>\n</tr>\n<tr>\n<td><code>business</code></td>\n<td>Business</td>\n</tr>\n<tr>\n<td><code>employment</code></td>\n<td>Employment</td>\n</tr>\n<tr>\n<td><code>corporate</code></td>\n<td>Corporate</td>\n</tr>\n<tr>\n<td><code>immigration</code></td>\n<td>Immigration</td>\n</tr>\n<tr>\n<td><em>(any normalized category name)</em></td>\n<td>e.g. <code>Real Estate</code> -&gt; <code>real_estate</code></td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"http-responses-sso\">HTTP responses (SSO)</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>200</strong></td>\n<td><code>{ \"loginUrl\": \"...\" }</code></td>\n</tr>\n<tr>\n<td><strong>400</strong></td>\n<td>Validation</td>\n</tr>\n<tr>\n<td><strong>401</strong></td>\n<td>Missing/wrong API key or HMAC</td>\n</tr>\n<tr>\n<td><strong>403</strong></td>\n<td>IP allowlist or SSO Login disabled</td>\n</tr>\n<tr>\n<td><strong>404</strong></td>\n<td>API key not recognized</td>\n</tr>\n<tr>\n<td><strong>409</strong></td>\n<td>Email is non-customer account</td>\n</tr>\n</tbody>\n</table>\n</div></body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[{"content":"LawVo inbound partner API","slug":"lawvo-inbound-partner-api"}],"owner":"55612606","collectionId":"4a6d35db-c5ae-4b22-984f-d31c2c43b002","publishedId":"2sBY4Mu1my","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-07-14T23:18:48.000Z"},"item":[{"name":"1. Quick start","item":[{"name":"Generate SSO link (required fields only)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"d17add2a-672f-493e-a454-bb4fa7805456"}}],"id":"776d093f-3e66-4925-bb3c-de66caa8dfac","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\"\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Smallest valid payload. No metaData, origin, or landingContext.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"776d093f-3e66-4925-bb3c-de66caa8dfac"},{"name":"Open one-time login URL (browser)","id":"542619d2-7339-4c9d-9885-45d7b0f4e917","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"","description":"<p>Run after a <strong>200</strong> generate. Enable <strong>Follow redirects</strong> in Postman.</p>\n<p>Opens <code>loginUrl</code> once. Second open fails (token consumed).</p>\n","urlObject":{"host":[""],"query":[],"variable":[]}},"response":[],"_postman_id":"542619d2-7339-4c9d-9885-45d7b0f4e917"}],"id":"693e502b-a8c0-463b-bee6-a75ad65b0c44","description":"<h2 id=\"purpose\">Purpose</h2>\n<p>Smallest path to get a one-time <code>loginUrl</code> and open it in the browser.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>First integration smoke test, or SSO-only login with no Document Hub extras.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>API Access + SSO Login enabled. Collection variables <code>baseUrl</code>, <code>PARTNER_API_KEY</code>, <code>PARTNER_HMAC_SECRET</code>. Required body fields only.</p>\n<h2 id=\"request-example\">Request example</h2>\n<p>Run <strong>Generate SSO link (required fields only)</strong>, then <strong>Open one-time login URL</strong>.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p><strong>200</strong> <code>{ \"loginUrl\": \"...\" }</code>. Consume once; second open fails.</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>401</strong></td>\n<td>Bad key/HMAC</td>\n</tr>\n<tr>\n<td><strong>403</strong></td>\n<td>IP or SSO Login off</td>\n</tr>\n<tr>\n<td><strong>404</strong></td>\n<td>Unknown key / API Access off</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Set credentials.</li>\n<li>Generate.</li>\n<li>Open <code>loginUrl</code> once with Follow redirects enabled.</li>\n</ol>\n","_postman_id":"693e502b-a8c0-463b-bee6-a75ad65b0c44"},{"name":"2. metaData examples","item":[{"name":"metaData: full (all contexts)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"eef82693-941d-43c0-9a0e-12e776f2fc3d"}}],"id":"10b0a678-5cd3-43c3-b302-52c5a50a1275","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"metaData\": {\n    \"personal\": {\n      \"firstName\": \"Jane\",\n      \"lastName\": \"Member\",\n      \"phoneNumber\": \"4165550100\",\n      \"emailAddress\": \"member.test+1784078655@example.com\",\n      \"dateOfBirth\": \"1990-04-15\"\n    },\n    \"business\": [\n      {\n        \"name\": \"Primary business\",\n        \"isDefault\": true,\n        \"businessName\": \"Acme PM Ltd.\",\n        \"businessAddress\": {\n          \"line1\": \"123 Main St\",\n          \"city\": \"Toronto\",\n          \"province\": \"ON\",\n          \"postalCode\": \"M5V 1A1\"\n        },\n        \"city\": \"Toronto\",\n        \"province\": \"ON\",\n        \"businessNumber\": \"123456789\",\n        \"companyName\": \"Acme PM\",\n        \"numberOfEmployees\": \"25\"\n      }\n    ],\n    \"family\": [\n      {\n        \"name\": \"Jane Doe\",\n        \"firstName\": \"Jane\",\n        \"lastName\": \"Doe\",\n        \"dateOfBirth\": \"1985-06-20\"\n      }\n    ],\n    \"employee\": [\n      {\n        \"name\": \"John Smith\",\n        \"firstName\": \"John\",\n        \"lastName\": \"Smith\",\n        \"email\": \"john.smith@acme.com\"\n      }\n    ]\n  }\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Canonical full prefill payload (all contexts).</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"10b0a678-5cd3-43c3-b302-52c5a50a1275"},{"name":"metaData: personal only","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"8ca288e2-ee01-4cab-a930-35b47e29fb30"}}],"id":"1e4bd4f2-54ae-4a1e-a3f8-c94cd50bd820","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"metaData\": {\n    \"personal\": {\n      \"dateOfBirth\": \"1992-08-10\"\n    }\n  }\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Minimal personal prefill. Top-level identity merges into personal when present.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"1e4bd4f2-54ae-4a1e-a3f8-c94cd50bd820"}],"id":"30073c2a-4107-4146-acb5-62f63f0ca5b0","description":"<h2 id=\"purpose\">Purpose</h2>\n<p>Optional <code>metaData</code> on generate SSO to prefill Document Hub personal/business/family/employee fields.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>Member already filled a profile on your site; you want those values ready in Document Hub after login.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>Same SSO auth as Quick start. Known keys are type-validated; unknown keys are accepted and stored but not prefilled. Dates: <code>YYYY-MM-DD</code>. Array items need <code>name</code>.</p>\n<h2 id=\"request-example\">Request example</h2>\n<p><strong>metaData: full</strong> for all contexts, or <strong>metaData: personal only</strong> for a minimal personal block. Omit <code>metaData</code> or send <code>{}</code> for SSO-only.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p><strong>200</strong> with <code>loginUrl</code> immediately. Prefill runs asynchronously (best effort) after the response.</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>400</strong></td>\n<td>Invalid known field (e.g. bad date) or array item missing <code>name</code></td>\n</tr>\n<tr>\n<td><strong>401/403/404</strong></td>\n<td>Same as SSO auth</td>\n</tr>\n</tbody>\n</table>\n</div><p>Other shapes (business-only, custom partner keys) follow the same request pattern; keep one full + one personal example here.</p>\n<h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Map your profile fields to known Document Hub keys.</li>\n<li>POST generate with <code>metaData</code>.</li>\n<li>Redirect member to <code>loginUrl</code>.</li>\n<li>Confirm prefill in Document Hub after login.</li>\n</ol>\n","_postman_id":"30073c2a-4107-4146-acb5-62f63f0ca5b0"},{"name":"3. Post-login landing (origin)","item":[{"name":"origin: document_hub","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"6a6ec28b-ab38-4537-8138-77438df7b186"}}],"id":"5046d08d-1d6c-4509-a7f4-0698507055c9","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"origin\": \"document_hub\"\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Land on <strong>Document Hub</strong> after consume.</p>\n<ul>\n<li><code>origin</code>: <code>document_hub</code></li>\n<li>Expected path: <code>/document-center/?key=3</code></li>\n</ul>\n<p>Other supported slugs: see collection description table.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"5046d08d-1d6c-4509-a7f4-0698507055c9"}],"id":"28689ec1-5cc3-4c6f-bd02-cee55fca4757","description":"<h2 id=\"purpose\">Purpose</h2>\n<p>Optional <code>origin</code> slug so the member lands on a LawVo section after SSO instead of the default dashboard.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>Member clicked Document Hub (or another product area) on your site and should open that section after login.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>Same SSO auth. <code>origin</code> must be a valid slug format. Supported slugs: dashboard, legal_ai, knowledge_center, document_hub, document_library, wills, legal_support, settings (see collection description table).</p>\n<h2 id=\"request-example\">Request example</h2>\n<p>This folder keeps one runnable example: <code>origin: document_hub</code>. Other slugs use the same shape with a different <code>origin</code> value.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p><strong>200</strong> <code>{ \"loginUrl\": \"...\" }</code>. After consume, the app opens the mapped section. Unknown but valid slugs fall back to the role default dashboard.</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>400</strong></td>\n<td>Invalid origin format (e.g. path like <code>/evil-path</code>)</td>\n</tr>\n<tr>\n<td><strong>401/403/404</strong></td>\n<td>Same as SSO auth</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Choose the section slug from the supported table.</li>\n<li>POST generate with <code>origin</code>.</li>\n<li>Redirect to <code>loginUrl</code> once.</li>\n<li>Confirm the member lands on the expected section.</li>\n</ol>\n","_postman_id":"28689ec1-5cc3-4c6f-bd02-cee55fca4757"},{"name":"4. Document Hub deep-links (landingContext)","item":[{"name":"landingContext: category filter only","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"5b167fe2-76e5-4a7b-8cee-b1bd06457001"}}],"id":"ccae22ea-dfa0-46c6-88f7-95f2a400b100","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"origin\": \"document_hub\",\n  \"landingContext\": {\n    \"serviceCategory\": \"will\"\n  }\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Filter My Templates to the Wills category. Uses slug <code>will</code> (not an internal id).</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"ccae22ea-dfa0-46c6-88f7-95f2a400b100"},{"name":"landingContext: create from template","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has loginUrl', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('loginUrl');","  pm.expect(json.loginUrl).to.include('/sso/consume/');","  pm.collectionVariables.set('lastLoginUrl', json.loginUrl);","});"],"id":"6e3df0d2-4ad6-4191-a56c-de785e80940c"}}],"id":"7a2fda30-1a9c-4d80-8b1d-cd6805cacd8b","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"origin\": \"document_hub\",\n  \"landingContext\": {\n    \"templateId\": 1,\n    \"action\": \"create\"\n  }\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Replace <code>templateId</code> with a template id from your environment. Opens guarded document creation.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"7a2fda30-1a9c-4d80-8b1d-cd6805cacd8b"},{"name":"landingContext: invalid inputs (400)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 400', () => pm.response.to.have.status(400));","pm.test('landingContext validation error', () => {","  const err = (pm.response.json() || {}).error || '';","  pm.expect(err).to.match(/serviceCategory|templateId/i);","});"],"id":"37c519ba-74ed-42ae-9832-c96a838cc4b5"}}],"id":"4bd15610-ad09-4a7c-95e1-93891c0f3137","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"landingContext\": {\n    \"serviceCategory\": \"123invalid\"\n  }\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Example invalid <code>serviceCategory</code> (must match slug format). Invalid <code>templateId</code> (non-integer) also returns <strong>400</strong>.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"4bd15610-ad09-4a7c-95e1-93891c0f3137"}],"id":"8acffcdd-3860-4c16-8125-c330c2caf891","description":"<h2 id=\"purpose\">Purpose</h2>\n<p><code>landingContext</code> on generate SSO deep-links the member into Document Hub with a category filter or a create-from-template intent.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>Member chose Wills (or Create Basic Will) on your site; after SSO they should see that category or start create for a known template.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>Same SSO auth + SSO Login. Prefer <code>origin: \"document_hub\"</code>. Fields: <code>serviceCategory</code> (slug), <code>templateId</code> (env-specific positive integer), <code>action: \"create\"</code> (requires <code>templateId</code>). Precedence: create intent &gt; category filter &gt; origin &gt; dashboard. Category slugs: will/wills, real_estate, family, business, employment, corporate, immigration (or normalized category name).</p>\n<h2 id=\"request-example\">Request example</h2>\n<p><strong>Category filter</strong> uses <code>serviceCategory: \"will\"</code>. <strong>Create from template</strong> uses <code>templateId</code> + <code>action: \"create\"</code> (replace id for your environment). If both are sent, create wins.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p><strong>200</strong> with <code>loginUrl</code>. After consume: category filter selects that category; create opens My Templates with the template sidebar (disclaimer + access checks still apply).</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>400</strong></td>\n<td>Invalid <code>serviceCategory</code> slug format or non-integer <code>templateId</code></td>\n</tr>\n<tr>\n<td><strong>401/403/404</strong></td>\n<td>Same as SSO auth</td>\n</tr>\n</tbody>\n</table>\n</div><p>Unknown-but-valid category slugs still return <strong>200</strong>; Document Hub shows all templates after login.</p>\n<h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Decide category browse vs create.</li>\n<li>Resolve <code>templateId</code> for the target env if creating (GET /inbound/document-templates).</li>\n<li>POST generate with <code>landingContext</code>.</li>\n<li>Redirect to <code>loginUrl</code> once.</li>\n<li>Confirm Document Hub landing behavior.</li>\n</ol>\n","_postman_id":"8acffcdd-3860-4c16-8125-c330c2caf891"},{"name":"5. Validation errors","item":[{"name":"missing email (400)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 400', () => pm.response.to.have.status(400));","pm.expect(pm.response.json().error).to.eql('email is required');"],"id":"57a98728-e490-4c48-a3ca-e823f7017e6d"}}],"id":"0ea18b76-fdc4-4fdd-ab38-21ce7c2b885c","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\"\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Required field missing.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"0ea18b76-fdc4-4fdd-ab38-21ce7c2b885c"},{"name":"invalid origin path (400)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 400', () => pm.response.to.have.status(400));","pm.expect(pm.response.json().error || '').to.match(/origin/i);"],"id":"02b6a9d1-1128-4def-bf84-a717c5bd8550"}}],"id":"290647ac-8b46-4b2e-a7a1-0ecbaaa0bdbc","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-API-Key","value":"","type":"text"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"member.test+1784078655@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\",\n  \"origin\": \"/evil-path\"\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Origin must be a slug, not a path.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"290647ac-8b46-4b2e-a7a1-0ecbaaa0bdbc"},{"name":"no auth headers (401)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 401', () => pm.response.to.have.status(401));"],"id":"afe1a263-406e-4d80-8e1f-9f72b0c11329"}}],"id":"1d182427-3307-454e-bfba-f314c6205d36","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Member\",\n  \"email\": \"noauth@example.com\",\n  \"phone\": \"4165550100\",\n  \"province\": \"ON\",\n  \"city\": \"Toronto\"\n}"},"url":"https://stagingapi.lawvo.com/generate-sso-link","description":"<p>Missing API key and HMAC.</p>\n","urlObject":{"path":["generate-sso-link"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"1d182427-3307-454e-bfba-f314c6205d36"},{"name":"Open SSO consume URL (invalid token - 302 error)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 302', () => pm.response.to.have.status(302));","pm.test('Redirect with error', () => {","  const loc = pm.response.headers.get('Location') || '';","  pm.expect(loc).to.include('error=');","});"],"id":"d032feca-090d-477b-b4fa-5fb35685de48"}}],"id":"f6b3cbce-a8bd-4fd3-9b70-08ff4adc7c53","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://stagingapi.lawvo.com/sso/consume/invalid-token-value-xyz","description":"<p>Consume endpoint. No partner auth. Invalid token redirects with <code>error=</code> query param.</p>\n","urlObject":{"path":["sso","consume","invalid-token-value-xyz"],"host":["https://stagingapi.lawvo.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"f6b3cbce-a8bd-4fd3-9b70-08ff4adc7c53"}],"id":"ee9db09b-ca28-4471-a2d3-861718826a91","description":"<h2 id=\"purpose\">Purpose</h2>\n<p>Canonical failure examples for SSO generate and consume.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>Verify your client handles validation and auth failures correctly.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>Use valid auth except for <strong>no auth headers</strong>. Invalid origin / landingContext cases are covered here and in folder 4.</p>\n<h2 id=\"request-example\">Request example</h2>\n<p>Run <strong>missing email</strong>, <strong>invalid origin path</strong>, <strong>no auth headers</strong>, and optionally the invalid consume URL.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p>Expected <strong>400</strong>, <strong>401</strong>, or consume <strong>302</strong> with <code>error=</code> as noted per request.</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>400</strong></td>\n<td>Missing/invalid email; invalid origin path; bad metaData date</td>\n</tr>\n<tr>\n<td><strong>401</strong></td>\n<td>No auth headers</td>\n</tr>\n<tr>\n<td><strong>302</strong></td>\n<td>Invalid consume token redirects with <code>error=</code></td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Run each request.</li>\n<li>Assert status and error message.</li>\n<li>Fix your client validation to match.</li>\n</ol>\n","_postman_id":"ee9db09b-ca28-4471-a2d3-861718826a91"},{"name":"6. Document templates","item":[{"name":"List (default)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Response has data array and pagination', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('data');","  pm.expect(json.data).to.be.an(\"array\");","  pm.expect(json).to.have.property('pagination');","  pm.expect(json.pagination).to.have.property('total');","  if (json.data.length > 0) {","    const item = json.data[0];","    pm.expect(item).to.have.property('title');","    pm.expect(item).to.have.property('categories');","    pm.expect(item).to.not.have.property('effectivePermission');","  }","});"],"id":"aa0ab822-ea7b-42e1-990e-4f0a7eb184d7"}}],"id":"50fa5cfc-400c-41ad-86e2-995c6f212b7a","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"X-API-Key","value":"","type":"text"}],"url":"https://stagingapi.lawvo.com/inbound/document-templates?locale=en&_start=0&_limit=25","description":"<p>English catalog page. Omit <code>locale</code> to return all locales. Returns <code>data</code> + <code>pagination</code>.</p>\n","urlObject":{"path":["inbound","document-templates"],"host":["https://stagingapi.lawvo.com"],"query":[{"key":"locale","value":"en"},{"key":"_start","value":"0"},{"key":"_limit","value":"25"}],"variable":[]}},"response":[],"_postman_id":"50fa5cfc-400c-41ad-86e2-995c6f212b7a"},{"name":"List with filters + pagination","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.response.to.have.status(200));","pm.test('Has pagination', () => {","  const json = pm.response.json();","  pm.expect(json).to.have.property('pagination');","  pm.expect(json.data).to.be.an('array');","});"],"id":"dd7f1ac1-c697-4264-9c0d-f0d821ad8423"}}],"id":"ec79aa87-9bdb-44b0-b954-ed74d8122f6a","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"X-API-Key","value":"","type":"text"}],"url":"https://stagingapi.lawvo.com/inbound/document-templates?locale=en&categoryId=3&page=1&pageSize=10","description":"<p>Filter by category and paginate with <code>page</code> / <code>pageSize</code>. Set <code>documentTemplatesCategoryId</code>. Tag filter uses <code>tagId</code> the same way (see markdown).</p>\n","urlObject":{"path":["inbound","document-templates"],"host":["https://stagingapi.lawvo.com"],"query":[{"key":"locale","value":"en"},{"key":"categoryId","value":"3"},{"key":"page","value":"1"},{"key":"pageSize","value":"10"}],"variable":[]}},"response":[],"_postman_id":"ec79aa87-9bdb-44b0-b954-ed74d8122f6a"},{"name":"no auth headers (401)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 401', () => pm.response.to.have.status(401));"],"id":"ffec0f59-5db0-40dc-9b9a-c6feb3fc10fa"}}],"id":"154a91a6-3602-429f-87ca-f0092f6a1834","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[],"url":"https://stagingapi.lawvo.com/inbound/document-templates?locale=en","description":"<p>Missing API key and HMAC.</p>\n","urlObject":{"path":["inbound","document-templates"],"host":["https://stagingapi.lawvo.com"],"query":[{"key":"locale","value":"en"}],"variable":[]}},"response":[],"_postman_id":"154a91a6-3602-429f-87ca-f0092f6a1834"},{"name":"Document Templates disabled (403)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 403', () => {","  const templatesOffKey = (pm.collectionVariables.get('PARTNER_API_KEY_TEMPLATES_OFF') || '').trim();","  const templatesOffSecret = (pm.collectionVariables.get('PARTNER_HMAC_SECRET_TEMPLATES_OFF') || '').trim();","  if (!templatesOffKey || !templatesOffSecret) {","    pm.expect.fail('Set PARTNER_API_KEY_TEMPLATES_OFF and PARTNER_HMAC_SECRET_TEMPLATES_OFF');","  }","  pm.response.to.have.status(403);","});"],"id":"85b0b4f4-6493-468d-9072-9e968b64e437"}}],"id":"f469f1d8-1d22-421a-8ae3-8c32772b5e36","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"GET","header":[{"key":"X-API-Key","value":"","type":"text"}],"url":"https://stagingapi.lawvo.com/inbound/document-templates?locale=en","description":"<p>Requires <code>PARTNER_API_KEY_TEMPLATES_OFF</code> and <code>PARTNER_HMAC_SECRET_TEMPLATES_OFF</code> for a group with Inbound API on but <strong>Document Templates</strong> toggle off.</p>\n<p>Expected body: <code>{ \"error\": \"Document Templates API is not enabled for this group\" }</code></p>\n","urlObject":{"path":["inbound","document-templates"],"host":["https://stagingapi.lawvo.com"],"query":[{"key":"locale","value":"en"}],"variable":[]}},"response":[],"_postman_id":"f469f1d8-1d22-421a-8ae3-8c32772b5e36"}],"id":"ed959e1e-d416-48dc-8b60-176c624c13ed","description":"<h2 id=\"purpose\">Purpose</h2>\n<p><strong>GET</strong> <code>/inbound/document-templates</code> returns paginated Basic Information metadata for templates your group may use.</p>\n<h2 id=\"use-case\">Use case</h2>\n<p>Build a template picker in your app, or resolve environment-specific <code>templateId</code> values for Document Hub deep-links.</p>\n<h2 id=\"required-inputs\">Required inputs</h2>\n<p>API Access + <strong>Document Templates</strong> enabled. Sign GET with canonical <code>GET\\n/inbound/document-templates\\n&lt;sorted query&gt;</code>. Optional query: <code>locale</code> (omit for all locales), <code>categoryId</code>, <code>tagId</code>, <code>_start</code>/<code>_limit</code> or <code>page</code>/<code>pageSize</code>.</p>\n<h2 id=\"request-example\">Request example</h2>\n<p><strong>List (default)</strong> for a basic catalog. <strong>List with filters + pagination</strong> for category + locale + page. Tag-only and locale-fr filters use the same query shape; they are not duplicated as separate examples.</p>\n<h2 id=\"expected-response\">Expected response</h2>\n<p><strong>200</strong> <code>{ data: [...], pagination: { start, limit, total } }</code>. Fields include id, title, subtitle, categories, tags, locale, type, subCategories, duration, samplePdfUrl. No pricing or effectivePermission.</p>\n<h2 id=\"common-errors\">Common errors</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>401</strong></td>\n<td>Missing/wrong key or HMAC</td>\n</tr>\n<tr>\n<td><strong>403</strong></td>\n<td>IP block or Document Templates off</td>\n</tr>\n<tr>\n<td><strong>404</strong></td>\n<td>Unknown key / API Access off</td>\n</tr>\n</tbody>\n</table>\n</div><p>Optional vars for the templates-off example: <code>PARTNER_API_KEY_TEMPLATES_OFF</code>, <code>PARTNER_HMAC_SECRET_TEMPLATES_OFF</code>.</p>\n<h2 id=\"complete-implementation-flow\">Complete implementation flow</h2>\n<ol>\n<li>Enable Document Templates for the group.</li>\n<li>Canonicalize query and sign.</li>\n<li>GET list.</li>\n<li>Use <code>id</code> values with landingContext create deep-links as needed.</li>\n</ol>\n","_postman_id":"ed959e1e-d416-48dc-8b60-176c624c13ed"}],"event":[{"listen":"prerequest","script":{"type":"text/javascript","exec":["const reqName = (pm.info.requestName || '').toLowerCase();","if (reqName.includes('no auth')) return;","if (reqName.includes('invalid hmac')) return;","if (reqName.includes('open one-time')) return;","if (reqName.includes('open sso consume')) return;","const urlStr = pm.request.url.toString();","const isDocumentTemplatesGet =","  pm.request.method === 'GET' && urlStr.includes('/inbound/document-templates');","let hmacSecret = (pm.collectionVariables.get('PARTNER_HMAC_SECRET') || '').trim();","const apiKeyHeaderRaw = pm.request.headers.get('X-API-Key') || '';","const apiKeyHeader = pm.variables.replaceIn(apiKeyHeaderRaw).trim();","const ipBlockedKey = (pm.collectionVariables.get('PARTNER_API_KEY_IP_BLOCKED') || '').trim();","const templatesOffKey = (pm.collectionVariables.get('PARTNER_API_KEY_TEMPLATES_OFF') || '').trim();","if (apiKeyHeader && apiKeyHeader === ipBlockedKey) {","  hmacSecret = (pm.collectionVariables.get('PARTNER_HMAC_SECRET_IP_BLOCKED') || hmacSecret).trim();","} else if (apiKeyHeader && apiKeyHeader === templatesOffKey) {","  hmacSecret = (pm.collectionVariables.get('PARTNER_HMAC_SECRET_TEMPLATES_OFF') || hmacSecret).trim();","}","if (!hmacSecret) {","  throw new Error('Set collection variable PARTNER_HMAC_SECRET (or matching secret for IP/templates-off keys).');","}","let message = '';","if (isDocumentTemplatesGet) {","  const path = '/inbound/document-templates';","  const urlObj = pm.request.url;","  let rawQuery = '';","  if (urlObj && urlObj.query && urlObj.query.count()) {","    const parts = [];","    urlObj.query.each(function (item) {","      if (item.disabled) return;","      parts.push(","        pm.variables.replaceIn(String(item.key || '')) +","          '=' +","          pm.variables.replaceIn(String(item.value ?? ''))","      );","    });","    rawQuery = parts.join('&');","  }","  const entries = rawQuery","    ? rawQuery.split('&').map(function (pair) {","        const idx = pair.indexOf('=');","        if (idx < 0) return [pair, ''];","        return [pair.slice(0, idx), pair.slice(idx + 1)];","      })","    : [];","  entries.sort(function (a, b) { return a[0].localeCompare(b[0]); });","  const query = entries","    .map(function (pair) {","      return encodeURIComponent(decodeURIComponent(pair[0])) + '=' + encodeURIComponent(decodeURIComponent(pair[1]));","    })","    .join('&');","  message = 'GET\\n' + path + '\\n' + query;","} else if (pm.request.method === 'POST') {","  const template =","    pm.request.body && pm.request.body.mode === 'raw' ? pm.request.body.raw || '' : '';","  const rawBody = pm.variables.replaceIn(template);","  if (pm.request.body && pm.request.body.mode === 'raw') {","    pm.request.body.raw = rawBody;","  }","  message = rawBody;","} else {","  return;","}","const sigHex = CryptoJS.HmacSHA256(message, hmacSecret).toString(CryptoJS.enc.Hex);","pm.request.headers.upsert({ key: 'X-Webhook-Signature', value: 'sha256=' + sigHex });"],"id":"912522b9-bcc3-4bd5-b67d-88d1b5a7f7c1"}}],"variable":[{"key":"baseUrl","value":"https://stagingapi.lawvo.com","description":"LawVo API host (no trailing slash). UAT or Live."},{"key":"PARTNER_API_KEY","value":"","description":"Inbound API key from LawVo (match environment to baseUrl)."},{"key":"PARTNER_HMAC_SECRET","value":"","description":"Inbound HMAC secret paired with your API key."},{"key":"lastLoginUrl","value":"","description":"Set after successful generate; used by Open one-time login URL."},{"key":"documentTemplatesCategoryId","value":"3","description":"Service category id for document template filter examples."},{"key":"documentTemplatesTagId","value":"98","description":"Tag id for document template filter examples."},{"key":"PARTNER_API_KEY_IP_BLOCKED","value":"","description":"Optional. API key for a group whose IP allowlist excludes your client (403 example)."},{"key":"PARTNER_HMAC_SECRET_IP_BLOCKED","value":"","description":"Optional. HMAC secret paired with PARTNER_API_KEY_IP_BLOCKED."},{"key":"PARTNER_API_KEY_TEMPLATES_OFF","value":"","description":"Optional. API key for a group with Document Templates toggle off (403 example)."},{"key":"PARTNER_HMAC_SECRET_TEMPLATES_OFF","value":"","description":"Optional. HMAC secret paired with PARTNER_API_KEY_TEMPLATES_OFF."}]}