Skip to content
New Webhooks added: Inventory and Order modifications. Check the changelog →
Cartly Developers

Customer Portal & B2B

Extend the Cartly customer portal with passwordless login, self-serve returns (RMA), headless customer API, portal extension points, B2B company hierarchy with RBAC, and enterprise OIDC SSO.

Overview

Customer Portal Extensibility (v1.6) ships in six phases:

  1. Phase 1 — Self-Serve Returns (RMA): Temporal-powered return workflow with merchant approve/reject signals.
  2. Phase 2 — Passwordless Login: OTP via email/SMS and Magic Links with 15-minute expiry.
  3. Phase 3 — Customer Headless API: SAT/CAT-authenticated profile, address, wishlist, and returns endpoints.
  4. Phase 4 — Portal Extension Points: Six named slots where apps inject Liquid, iframe, or remote UI blocks.
  5. Phase 5 — B2B Company Hierarchy: Companies, locations, memberships, role-based access control, and Temporal budget-reset cron.
  6. Phase 6 — Enterprise SSO (OIDC): OIDC init/callback flow with auto-membership and AES-GCM encrypted client secrets.

Passwordless Login

Customers log in via a one-time code (OTP) or a signed magic link. Both return a Customer Access Token (CAT) identical to the password-based login token.

OTP endpoints

  • POST /storefront/v1/customer/otp/request — send OTP to email or SMS
  • POST /storefront/v1/customer/otp/verify — verify code, returns CAT

Magic Link endpoints

  • POST /storefront/v1/customer/magic-link/request — send signed link to email
  • GET /storefront/v1/customer/magic-link/verify?token=... — verify token, returns CAT

SMS delivery uses the configured SMS_PROVIDER (twilio, sns, or mock). Magic links are signed JWTs with a 15-minute expiry.

OTP Login Flow

typescript
const SAT = 'sat_your_token';
const BASE = 'https://cartly.pro/storefront/v1';

// Step 1: request OTP
await fetch(`${BASE}/customer/otp/request`, {
  method: 'POST',
  headers: { 'X-Shopify-Storefront-Access-Token': SAT, 'Content-Type': 'application/json' },
  body: JSON.stringify({ identifier: 'user@example.com', channel: 'email' }),
});

// Step 2: verify OTP → returns CAT
const { access_token } = await fetch(`${BASE}/customer/otp/verify`, {
  method: 'POST',
  headers: { 'X-Shopify-Storefront-Access-Token': SAT, 'Content-Type': 'application/json' },
  body: JSON.stringify({ identifier: 'user@example.com', code: '847291' }),
}).then(r => r.json());

Customer Headless API

All customer mutation endpoints under /storefront/v1/customer/ require both a SAT (X-Shopify-Storefront-Access-Token) and a CAT (Authorization: Bearer <cat>).

  • PUT /storefront/v1/customer/profile — update name, phone
  • GET/POST/PUT/DELETE /storefront/v1/customer/addresses — address CRUD
  • GET/POST/DELETE /storefront/v1/customer/wishlist — wishlist
  • GET /storefront/v1/customer/returns — list return requests

App Session Token

Exchange a CAT for a short-lived App Session Token (AST) so an embedded app can act on behalf of the logged-in customer without storing credentials.

  • POST /storefront/v1/customer/app-session — issue AST (requires SAT + CAT)
  • POST /apps/api/customer-session/verify — verify AST server-side with the OAuth token of the app it was issued to, in the same shop (another app or shop → 401, an API key → 403); returns customer_id, email, scopes

Portal Extension Points

Apps attach one of their theme blocks (Liquid, iframe, or remote UI — registered first with POST /apps/api/theme-extensions) to one of six extension points in the customer portal:

  • Customer::Account::Navigation — additional navigation items in the account sidebar
  • Customer::Account::Dashboard — widgets on the account dashboard
  • Customer::Order::Details — block below the order details
  • Customer::Order::Actions — additional action buttons on an order
  • Customer::Profile::Section — section on the profile page
  • Customer::Return::Form — additional fields in the return form

Register: POST /apps/api/blocks/:blockSlug/portal-extensions with {"extension_point": "Customer::Account::Dashboard", "position": 0, "config": {…}} — needs theme_extensions:write and an OAuth app token; an unknown point, a config key the block's schema doesn't declare, or a config over 4 KB of JSON (it reaches the block's iframe as the ?config= query parameter) is a 400
Query active extensions: GET /storefront/v1/portal/extensions?point=Customer::Account::Dashboard
Admin management: GET/PUT/DELETE /admin/customer-portal/extensions

B2B Company Hierarchy

Three new entities support B2B commerce: Company, CompanyLocation, and CompanyMembership. Memberships carry a role: admin, buyer, or viewer. The RequireB2BRole middleware enforces access at the handler level.

Admin API

  • GET/POST/PUT/DELETE /admin/companies
  • GET/POST /admin/companies/:id/locations
  • GET/POST /admin/companies/:id/members

Customer API (CAT required)

  • GET /customer/company
  • GET /customer/company/members
  • GET /customer/company/locations
  • POST /customer/orders/:id/reorder

Location budgets reset automatically via a Temporal cron workflow. Set budget_period to monthly, quarterly, or yearly.

Enterprise SSO (OIDC)

Connect a company to an OIDC identity provider so buyers log in with corporate credentials. Supported: Google Workspace, Azure AD, Okta, custom OIDC.

  • PUT /admin/companies/:id/sso — configure OIDC (client secrets encrypted with AES-GCM)
  • GET /storefront/v1/customer/sso/init?company_id=... — start OIDC flow
  • GET /storefront/v1/customer/sso/callback — handle provider callback, issue CAT, auto-create membership

Members whose email domain matches email_domain in the SSO config are automatically linked to the company on first SSO login.

Self-Serve Returns (RMA)

Customers submit return requests from the order detail page. Each request starts a Temporal RMAWorkflow waiting for a merchant signal (approve/reject).

  • POST /customer/returns — submit return request (customer)
  • GET /customer/returns — list return requests (customer)
  • GET /storefront/v1/customer/returns — headless variant (SAT + CAT)

On approval, three activities run in sequence: ValidateReturn → NotifyMerchant → ProcessRefund. On rejection, the workflow completes with a notification to the customer. All activities are idempotent.