Use the same REST API that powers the StrikeUp Customer Portal to build a fully custom interface inside your own product.
Who is this for?
You should use the Portal API directly if you want to:
Embed customer-management UI inside your own app instead of using the StrikeUp Portal modal.
Match the customer experience pixel-for-pixel with your own design system.
Build a thin server-side proxy that calls the Portal API on behalf of authenticated tenant customers.
Add fields, custom flows, or analytics that are not part of the built-in Portal.
If you just want the off-the-shelf Portal, use the bundled widget UI — see Customer Portal in this doc set.
Authentication overview
The Portal API uses a separate identity scheme from the StrikeUp admin API. Customers authenticate with their own JWT (audience strikeup-customer), delivered as HttpOnly cookies and bound to the browser they were issued on.
Cookie
HttpOnly
Purpose
sb_access
Yes
Customer JWT used to authorise every Portal call.
sb_refresh
Yes
Opaque refresh token, rotated on every refresh.
sb_bind
No
Browser-binding nonce. JS reads it and sends as X-Browser-Binding on real-time upgrades. A mismatch immediately revokes the session.
All requests must include cookies — set credentials: 'include' on every fetch() call and configure your domain CORS to permit credentialed requests from your UI origin.
Getting a customer session
You have three options for establishing a customer session before calling any Portal endpoint:
Direct customer login
The customer types their PIN or password into your UI; you call the login endpoint, the backend sets cookies.
POST /api/customer-auth/loginContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"1234","rememberMe":true}→ 200OKSets cookies:sb_access,sb_refresh,sb_bind
POST /api/customer-auth/loginContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"1234","rememberMe":true}→ 200OKSets cookies:sb_access,sb_refresh,sb_bind
POST /api/customer-auth/loginContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"1234","rememberMe":true}→ 200OKSets cookies:sb_access,sb_refresh,sb_bind
Tenant-issued login ticket (Mode A)
If your backend already knows who the user is (your own SSO session), request a one-shot login ticket server-to-server, hand it to the browser, redeem it for cookies. The customer never types a credential in your UI.
Tickets are single-use, short-lived, and only valid for the customerId / widgetId pair they were issued for.
Self-registration
When the customer company allows self-registration, your UI can create the customer in one call (it both registers and signs in).
POST /api/customer-auth/registerContent-Type:application/json{"customerId":"alice@example.com","customerCompanyId":"cc_xxx","widgetId":"wgt_abc123","credential":"1234","credentialType":"Pin","name":"Alice"}→ 201Created — session cookies set
POST /api/customer-auth/registerContent-Type:application/json{"customerId":"alice@example.com","customerCompanyId":"cc_xxx","widgetId":"wgt_abc123","credential":"1234","credentialType":"Pin","name":"Alice"}→ 201Created — session cookies set
POST /api/customer-auth/registerContent-Type:application/json{"customerId":"alice@example.com","customerCompanyId":"cc_xxx","widgetId":"wgt_abc123","credential":"1234","credentialType":"Pin","name":"Alice"}→ 201Created — session cookies set
Driving the sign-in UX
Before showing a sign-in form, ask the backend what state the customer is in:
GET /api/customer-auth/probe?customerId=alice@example.com&widgetId=wgt_abc123
→ 200{"exists":true,"credentialIsSet":true,"credentialType":"Pin",// "Pin" | "Password""isDisabled":false,"isLocked":false}
GET /api/customer-auth/probe?customerId=alice@example.com&widgetId=wgt_abc123
→ 200{"exists":true,"credentialIsSet":true,"credentialType":"Pin",// "Pin" | "Password""isDisabled":false,"isLocked":false}
GET /api/customer-auth/probe?customerId=alice@example.com&widgetId=wgt_abc123
→ 200{"exists":true,"credentialIsSet":true,"credentialType":"Pin",// "Pin" | "Password""isDisabled":false,"isLocked":false}
Use the response to pick the right screen — first-time setup, standard login, locked-out, or disabled.
First-time setup hits a different endpoint than login:
POST /api/customer-auth/setup-credentialContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"newPIN1234","credentialType":"Pin"}→ 200OK — session cookies set
POST /api/customer-auth/setup-credentialContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"newPIN1234","credentialType":"Pin"}→ 200OK — session cookies set
POST /api/customer-auth/setup-credentialContent-Type:application/json{"customerId":"alice@example.com","widgetId":"wgt_abc123","credential":"newPIN1234","credentialType":"Pin"}→ 200OK — session cookies set
Knowing which pages to render
The tenant configures which Portal pages are visible per widget. Read the flags before rendering your sidebar:
GET /api/TokenValidation/validate/{applicationToken}?widgetSetup=true→ 200{"isValid":true,"requiresEndUserAuth":true,"portalEnabled":true,"portalMembersEnabled":true,"portalKnowledgeBaseEnabled":false,"portalSkillsEnabled":true,"portalWorkflowsEnabled":true,"portalIntegrationsEnabled":true,"portalSettingsEnabled":true}
GET /api/TokenValidation/validate/{applicationToken}?widgetSetup=true→ 200{"isValid":true,"requiresEndUserAuth":true,"portalEnabled":true,"portalMembersEnabled":true,"portalKnowledgeBaseEnabled":false,"portalSkillsEnabled":true,"portalWorkflowsEnabled":true,"portalIntegrationsEnabled":true,"portalSettingsEnabled":true}
GET /api/TokenValidation/validate/{applicationToken}?widgetSetup=true→ 200{"isValid":true,"requiresEndUserAuth":true,"portalEnabled":true,"portalMembersEnabled":true,"portalKnowledgeBaseEnabled":false,"portalSkillsEnabled":true,"portalWorkflowsEnabled":true,"portalIntegrationsEnabled":true,"portalSettingsEnabled":true}
Profile is always visible — there is no flag for it. The other six toggles map 1:1 to Portal pages. Default to true for any flag that is missing from the response so older backends keep working.
Endpoint inventory
Customer auth — /api/customer-auth/*
Verb
Path
Auth
Purpose
GET
/probe
Anonymous
Probe customer + widget to drive the sign-in UX.
POST
/login
Anonymous
Verify credential, issue cookies.
POST
/setup-credential
Anonymous
First-time credential setup.
POST
/register
Anonymous
Self-service open signup.
POST
/refresh
Refresh cookie
Rotate session. Reuse detection revokes the family on replay.
POST
/logout
Refresh cookie
Revoke this session, clear cookies.
GET
/me
Customer
Profile of the current customer.
POST
/change-credential
Customer
Change own credential. Revokes other live sessions.
POST
/issue-ticket
Tenant JWT
Mode A — server-side login ticket.
POST
/redeem-ticket
Anonymous
Mode A — redeem ticket for cookies.
Customer Portal — /api/portal/*
All endpoints require a Customer JWT (the sb_access cookie). Admin-only routes additionally check that the customer’s role is Admin.
Verb
Path
Role
Purpose
GET
/me
Member+
Own profile.
PUT
/me
Member+
Update own name / email.
GET
/customer-company
Member+
Own customer company.
PUT
/customer-company
Admin
Update self-registration / default credential type / agent instruction.
GET
/members
Admin
List members of the caller’s company.
PUT
/members/{id}/role
Admin
Change a member’s role. Guard prevents removing the last admin.
constAPI = "https://api.your-strikeup-tenant.com";constwidgetId = "wgt_abc123";constcustomerId = "alice@example.com";constopts = {credentials:"include"asconst};// 1) Probe — decide which screen to renderconstprobe = awaitfetch(`${API}/api/customer-auth/probe?customerId=${customerId}&widgetId=${widgetId}`,opts,).then(r=>r.json());if(!probe.exists || !probe.credentialIsSet){// → show first-time setup screen (POST /setup-credential)}else{// 2) Loginawaitfetch(`${API}/api/customer-auth/login`,{...opts,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({customerId,widgetId,credential:"1234"}),});}// 3) Profile — cookies are sent automaticallyconstme = awaitfetch(`${API}/api/portal/me`,opts).then(r=>r.json());// 4) Members — admin onlyif(me.role === "Admin"){constmembers = awaitfetch(`${API}/api/portal/members`,opts)
.then(r=>r.json());}
constAPI = "https://api.your-strikeup-tenant.com";constwidgetId = "wgt_abc123";constcustomerId = "alice@example.com";constopts = {credentials:"include"asconst};// 1) Probe — decide which screen to renderconstprobe = awaitfetch(`${API}/api/customer-auth/probe?customerId=${customerId}&widgetId=${widgetId}`,opts,).then(r=>r.json());if(!probe.exists || !probe.credentialIsSet){// → show first-time setup screen (POST /setup-credential)}else{// 2) Loginawaitfetch(`${API}/api/customer-auth/login`,{...opts,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({customerId,widgetId,credential:"1234"}),});}// 3) Profile — cookies are sent automaticallyconstme = awaitfetch(`${API}/api/portal/me`,opts).then(r=>r.json());// 4) Members — admin onlyif(me.role === "Admin"){constmembers = awaitfetch(`${API}/api/portal/members`,opts)
.then(r=>r.json());}
constAPI = "https://api.your-strikeup-tenant.com";constwidgetId = "wgt_abc123";constcustomerId = "alice@example.com";constopts = {credentials:"include"asconst};// 1) Probe — decide which screen to renderconstprobe = awaitfetch(`${API}/api/customer-auth/probe?customerId=${customerId}&widgetId=${widgetId}`,opts,).then(r=>r.json());if(!probe.exists || !probe.credentialIsSet){// → show first-time setup screen (POST /setup-credential)}else{// 2) Loginawaitfetch(`${API}/api/customer-auth/login`,{...opts,method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({customerId,widgetId,credential:"1234"}),});}// 3) Profile — cookies are sent automaticallyconstme = awaitfetch(`${API}/api/portal/me`,opts).then(r=>r.json());// 4) Members — admin onlyif(me.role === "Admin"){constmembers = awaitfetch(`${API}/api/portal/members`,opts)
.then(r=>r.json());}
Bulk pre-create customers (with optional server credentials)
Tenants can provision customers ahead of time — either via CSV upload or a structured JSON batch. Both paths accept an optional serverAuthorizations array per customer, which pre-fills the per-server API credentials (ServerCustomerAuthorization rows) in the same atomic transaction as the customer create. The end user lands directly in the widget — they enter a PIN and the backend already has their downstream API credentials ready.
Verb
Path
Body
Purpose
POST
/api/customer-import/preview
multipart/form-data (file)
Validate a CSV without writing.
POST
/api/customer-import/commit
multipart/form-data (file)
Commit the CSV; all-or-nothing.
POST
/api/customer-import/preview-json
application/json
JSON-shaped preview — same semantics as the CSV preview.
POST
/api/customer-import/commit-json
application/json
JSON-shaped commit — same atomic all-or-nothing semantics.
All four endpoints authenticate as a StrikeUp staff user (tenant JWT — not a customer cookie). They are not part of /api/portal/* — a customer can never call them.
serverId — must belong to your tenant AND be mapped to at least one of your widgets. Servers not mapped to any widget are rejected.
authType — inferred server-side from Server.AuthorizationType. Servers with AuthorizationType="None" are rejected — no credentials are accepted by that server.
authData — dictionary of header name → value. Keys must match the HTTP headers the upstream API expects (same shape as the widget’s in-product auth-update flow).
Atomic semantics
Both the customer create AND each ServerCustomerAuthorization upsert run inside one Company-DB transaction. A single auth-row validation failure (unknown server, server not mapped to a widget, AuthorizationType=None, empty authData) rolls back the whole batch — there is no partial state where the customer exists but the auth row is missing.
Re-running the same payload is idempotent — SaveServerAuthorizationAsync upserts on {customer.CustomerId, server.Id}. Useful for rotating credentials without re-creating customers.
Access cookies are short-lived; refresh cookies are long-lived and rotate on every call. Refresh proactively when an authenticated call returns 401:
POST /api/customer-auth/refresh?widgetId=wgt_abc123
→ 200OKSet-Cookie:sb_access(rotated),sb_refresh(rotated)X-Browser-Binding(header):must match the sb_bind cookie value
POST /api/customer-auth/refresh?widgetId=wgt_abc123
→ 200OKSet-Cookie:sb_access(rotated),sb_refresh(rotated)X-Browser-Binding(header):must match the sb_bind cookie value
POST /api/customer-auth/refresh?widgetId=wgt_abc123
→ 200OKSet-Cookie:sb_access(rotated),sb_refresh(rotated)X-Browser-Binding(header):must match the sb_bind cookie value
Reuse detection: replaying a stale refresh token invalidates the whole token family. Your UI should treat any non-200 from /refresh as a forced sign-out.
Error responses
Status
Meaning
400
Validation error — check the response body for details.
401
Not authenticated, expired session, or browser-binding mismatch. Try refresh; otherwise sign out.
403
Authenticated but role too low (e.g. member trying to hit an admin endpoint) — or attempting to act outside your own customer company.
404
Resource doesn’t exist or isn’t in your customer company.
409
Conflict — e.g. trying to demote the last admin, or registering a customerId that already exists.
423
Account locked after repeated credential failures. Wait out the lockout or reset the credential.
Cross-origin & cookie checklist
Your UI origin must be allow-listed on the StrikeUp tenant for credentialed CORS. Reach out if it’s not.
Always send credentials: 'include' on fetch — without it cookies won’t flow.
Cookies are issued with Secure; SameSite=None, so all calls must be over HTTPS.
The browser-binding cookie (sb_bind) is not HttpOnly; your client should attach it as the X-Browser-Binding header on real-time connections (SignalR / WebSocket upgrades).
Never proxy customer credentials through a server endpoint that isn’t HTTPS. Use ticket-redemption (Mode A) if you want zero-credential flows.
Best practices
Use the probe endpoint first. It tells you whether to show first-time setup, login, locked, or disabled screens — saving a round of failed login attempts.
Pre-fetch /api/TokenValidation/validate?widgetSetup=true on app boot to know which Portal pages to render. Default any missing flag to true.
Treat the customer’s role as the source of truth for client-side gating. Always re-validate on the server too — the API enforces the same checks.
On any 401 response, try /refresh once before forcing the user to sign in again.
For Mode A integrations, keep the ticket window narrow — issue the ticket inside the same HTTPS request that loads the page, redeem within seconds.
Don’t poll /me — fetch it once after login and cache. Re-fetch only after a credential change or a profile-edit response.
See also
Customer Portal — what the bundled Portal UI looks like and what each page does.
Authentication — tenant-side authorization and credential handling.