Scell Onboarding Widget — Developer Documentation
Package: @scell/onboarding-widget v2.0.0 Custom element: <scell-onboarding> Build output: ES module + IIFE (UMD CJS also exported)
Table of Contents
- Overview
- Installation
- Quick Start
- Configuration
- Onboarding Flow — The 3 Steps
- Events and Callbacks
- Customization
- Integration — React
- Integration — Vue 3
- Integration — Vanilla JS
- Public API Reference
- Troubleshooting
1. Overview
The Scell Onboarding Widget is a framework-agnostic Web Component (<scell-onboarding>) that embeds a complete B2B onboarding flow into any web page. It is designed for partner platforms that delegate the Scell account creation process to their end customers.
What it does
- Guides a business user through a 3-step onboarding delegated entirely to SuperPDP
- Opens a SuperPDP OAuth2 popup that handles account creation, KYB, and identity verification
- Closes the popup and shows a success screen once SuperPDP returns the authorization code
- Returns an
authorization_codeto the partner platform upon completion, which can then be exchanged for tenant credentials via the Scell Partner API
Why it exists
Partners integrating Scell (invoicing, signatures) need their own clients to self-onboard. Rather than building a custom KYB UI, partners embed this widget. The widget delegates the entire KYB process to SuperPDP via OAuth2 and receives a callback with the authorization code. The partner exchanges that code for API credentials using POST /v1/onboarding/exchange.
What changed from v1
v1 (deprecated) handled KYB internally: SIRET verification via INSEE, manual document uploads, representative form. v2 delegates everything to SuperPDP. The widget HTML interface and events are backward-compatible where possible, but the step names and some events have changed (see sections 5 and 6).
Technical design
- Implemented as a native Custom Element (
HTMLElementsubclass) - Uses Shadow DOM (mode:
open) for complete style encapsulation — host page CSS cannot bleed in - Zero runtime dependencies (no React, Vue, Angular, or any external library)
- Communicates with the Scell API using standard
fetch, headersX-Publishable-KeyandX-Session-Id - Opens a SuperPDP authorization popup via
window.openand listens for apostMessagefrom the backend callback page - Emits standard
CustomEventinstances that bubble up through the DOM - Supports
light,dark, andauto(OS preference) themes via CSS custom properties exposed on:host - Fully responsive, tested down to 320 px viewport width
2. Installation
npm / pnpm / yarn
npm install @scell/onboarding-widget
# or
pnpm add @scell/onboarding-widget
# or
yarn add @scell/onboarding-widgetThe package ships pre-built in dist/. No build step required on the consumer side.
CDN (script tag — IIFE build)
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>Or pin to a specific version:
<script src="https://cdn.scell.io/widget/1.1.0/onboarding.js"></script>The IIFE build registers the <scell-onboarding> custom element automatically and exposes window.ScellOnboarding.
ES module (CDN)
<script type="module">
import '@scell/onboarding-widget';
</script>Or from a CDN supporting ESM:
<script type="module" src="https://cdn.scell.io/widget/v1/onboarding.esm.js"></script>3. Quick Start
Minimum viable integration — 5 lines of HTML:
<!DOCTYPE html>
<html>
<body>
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>
<scell-onboarding publishable-key="pk_test_YOUR_KEY"></scell-onboarding>
</body>
</html>The widget renders at full width and 600 px tall by default. The environment (sandbox or production) is determined automatically from the publishable key prefix (pk_test_* → sandbox, pk_live_* → production). Labels are in French and the light theme is used.
4. Configuration
Configuration is passed as HTML attributes on the <scell-onboarding> element. All attributes are reactive: changing them after mount updates the widget.
Attributes reference
| Attribute | Type | Default | Required | Description |
|---|---|---|---|---|
publishable-key | string | — | Yes | Your Scell publishable key (pk_live_... or pk_test_...). The widget will not initialize without this. The key prefix determines the environment automatically: pk_test_* → sandbox, pk_live_* → production. |
theme | "light" | "dark" | "auto" | "light" | No | Color scheme. "auto" respects the OS-level prefers-color-scheme media query. |
locale | "fr" | "en" | "fr" | No | Interface language. All labels, error messages, and placeholders are translated. |
external-id | string | — | No | Your internal customer or user identifier. Forwarded as-is in the onboarding:completed event payload and in the callback URL query string as external_id. Useful to correlate the Scell session with your own records. |
callback-url | string | — | No | If provided, the browser is redirected to this URL 2 seconds after a successful submission. The authorization code is appended as ?code=AUTH_CODE. If external-id is also set, &external_id=... is also appended. |
width | string | "100%" | No | CSS width of the host element (any valid CSS length: 480px, 100%, 50vw…). |
height | string | "600px" | No | CSS height of the host element. The inner content scrolls if it overflows. 650px is recommended for the documents step. |
HTML example with all options
<scell-onboarding
publishable-key="pk_live_a1b2c3d4e5f6"
theme="auto"
locale="en"
external-id="customer_7890"
callback-url="https://app.example.com/onboarding/callback"
width="520px"
height="680px"
></scell-onboarding>Changing attributes dynamically
const widget = document.querySelector('scell-onboarding');
// Switch to dark theme at runtime
widget.setAttribute('theme', 'dark');
// Switch locale
widget.setAttribute('locale', 'en');Note: Only theme triggers a full re-render when changed after mount. Other attribute changes update the internal config but do not re-render the current step.
5. Onboarding Flow — The 3 Steps
The widget walks users through a linear, guided flow. KYB, identity verification, and account creation are handled entirely by SuperPDP via an OAuth2 popup — the widget does not collect company or representative data directly.
Progress is tracked by the API session. If the user closes the popup without completing, they can click the connect button again to reopen it.
Step 1 — Connect (connect)
Purpose: Initiate the SuperPDP OAuth2 authorization.
What the user sees:
- A brief explanation of what will happen (SuperPDP will verify their company and identity)
- A "Connecter avec SuperPDP" / "Connect with SuperPDP" button
What happens behind the scenes:
- On button click:
POST /api/v1/onboarding/superpdp/authorizewith the session ID - The API returns a
{ authorize_url }pointing to the SuperPDP authorization endpoint - The widget calls
window.open(authorize_url, 'superpdp_auth', 'width=600,height=700')to open the popup - The widget transitions to step 2 and begins listening for a
postMessagefrom the popup origin
Layout:
+----------------------------------+
| [scell.io logo] |
| [1] Connect > [2] > [3] |
| |
| Vérifiez votre entreprise |
| SuperPDP va guider la |
| vérification de votre identité |
| et de votre entreprise. |
| |
| [ Connecter avec SuperPDP ] |
+----------------------------------+Step 2 — Redirect (redirect)
Purpose: Wait for the user to complete the SuperPDP flow in the popup.
What the user sees:
- A loading/waiting screen with a spinner or animated indicator
- A message indicating they should complete the process in the popup
- A "Réouvrir la fenêtre" / "Reopen window" link in case the popup was closed or blocked
What happens behind the scenes:
- The widget waits for a
postMessagefrom the SuperPDP callback page (served by the Scell backend) - When the backend
POST /api/v1/onboarding/superpdp/callbackreceives the OAuth2 code, it exchanges it for tokens, creates the tenant, and posts{ type: 'superpdp:success', authorizationCode }to the opener window - On receiving the message, the widget closes the popup and transitions to step 3
- On
{ type: 'superpdp:error' }: inline error with a retry button
Popup blocked: If window.open returns null, the widget shows an inline message asking the user to allow popups and provides a direct link to the authorization URL.
Layout:
+----------------------------------+
| [1 done] > [2 ACTIVE] > [3] |
| |
| Vérification en cours... |
| Complétez le processus dans |
| la fenêtre SuperPDP. |
| |
| [spinner] |
| |
| Fenêtre fermée ? |
| [ Réouvrir la fenêtre ] |
+----------------------------------+Step 3 — Complete (complete)
Purpose: Confirm success and deliver the authorization code.
What the user sees:
- A success message confirming the account is ready
- No form — all data was collected by SuperPDP
What happens behind the scenes:
- The
onboarding:completedevent fires with theauthorizationCode - If
callback-urlis set, the browser is redirected after 2 seconds
Layout:
+----------------------------------+
| [1 done] > [2 done] > [3 DONE] |
| |
| Compte créé avec succès ! |
| Votre espace Scell est prêt. |
| |
| [checkmark icon] |
| |
| Vous allez être redirigé... |
+----------------------------------+6. Events and Callbacks
The widget communicates with the host page exclusively through CustomEvents. All events bubble up the DOM and are composed (they cross Shadow DOM boundaries).
Listening to events
const widget = document.querySelector('scell-onboarding');
widget.addEventListener('onboarding:completed', (event) => {
console.log(event.detail.authorizationCode);
});Or at the document level (events bubble):
document.addEventListener('onboarding:completed', (event) => {
// event.target is the <scell-onboarding> element
exchangeCodeWithBackend(event.detail.authorizationCode);
});Event reference
onboarding:started
Fired once, immediately after the API session is created.
interface OnboardingStartedPayload {
sessionId: string; // UUID of the created onboarding session
}Use case: Store the session ID in case you need to check status via your own backend.
widget.addEventListener('onboarding:started', (e) => {
localStorage.setItem('scell_session_id', e.detail.sessionId);
});onboarding:step
Fired every time the widget transitions to a different step.
interface OnboardingStepPayload {
step: 'connect' | 'redirect' | 'complete';
progress: number; // 0, 50, or 100
}progress values: connect → 0, redirect → 50, complete → 100.
widget.addEventListener('onboarding:step', (e) => {
updateProgressBar(e.detail.progress);
});onboarding:popup-opened
Fired when the SuperPDP authorization popup is successfully opened.
interface PopupOpenedPayload {
authorizeUrl: string; // The SuperPDP URL loaded in the popup
}Use case: analytics, logging, or displaying a "popup is open" notice to the user.
widget.addEventListener('onboarding:popup-opened', (e) => {
console.log('SuperPDP popup opened:', e.detail.authorizeUrl);
});onboarding:completed
Fired after the SuperPDP callback is processed and the tenant is created. This is the primary event your integration must handle.
interface OnboardingCompletedPayload {
authorizationCode: string; // One-time code — exchange immediately
externalId?: string; // Reflects the external-id attribute if set
}Critical: The authorizationCode is short-lived. Exchange it for tenant credentials via POST /v1/onboarding/exchange as soon as possible. Do not expose it in browser storage.
widget.addEventListener('onboarding:completed', async (e) => {
const { authorizationCode, externalId } = e.detail;
// Send to YOUR backend, which calls the Scell Partner API
const response = await fetch('/api/onboarding/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: authorizationCode, customerId: externalId })
});
const { tenantApiKey } = await response.json();
// Store tenantApiKey for this customer
});onboarding:error
Fired on initialization errors (failed session creation) or any unrecoverable API error.
interface OnboardingErrorPayload {
code: string; // Machine-readable error code, e.g. "INIT_ERROR", "INVALID_PARTNER_KEY"
message: string; // Human-readable description
}widget.addEventListener('onboarding:error', (e) => {
console.error(`[Scell] ${e.detail.code}: ${e.detail.message}`);
showFallbackUI();
});TypeScript types for event handlers
If you use TypeScript, you can augment HTMLElementEventMap for type-safe listeners:
import type {
OnboardingStartedPayload,
OnboardingStepPayload,
PopupOpenedPayload,
OnboardingCompletedPayload,
OnboardingErrorPayload
} from '@scell/onboarding-widget';
// Declare the custom element type
declare global {
interface HTMLElementTagNameMap {
'scell-onboarding': import('@scell/onboarding-widget').ScellOnboarding;
}
interface HTMLElementEventMap {
'onboarding:started': CustomEvent<OnboardingStartedPayload>;
'onboarding:step': CustomEvent<OnboardingStepPayload>;
'onboarding:popup-opened': CustomEvent<PopupOpenedPayload>;
'onboarding:completed': CustomEvent<OnboardingCompletedPayload>;
'onboarding:error': CustomEvent<OnboardingErrorPayload>;
}
}7. Customization
7.1 Theme — light, dark, auto
Set via the theme attribute:
<scell-onboarding theme="dark" publishable-key="pk_test_..."></scell-onboarding>"auto" uses window.matchMedia('(prefers-color-scheme: dark)') at render time. It does not reactively update if the OS preference changes while the widget is mounted — use attributeChangedCallback or re-set the attribute from an matchMedia listener if dynamic switching is needed.
7.2 CSS Custom Properties
The widget exposes design tokens as CSS custom properties on :host. Because Shadow DOM isolates internal styles, these properties must be set on the host element from outside:
scell-onboarding {
--scell-primary: #6366f1; /* Main accent color (buttons, links, active step) */
--scell-primary-hover: #818cf8; /* Hover state */
--scell-primary-active: #4f46e5; /* Active/pressed state */
--scell-background: #ffffff; /* Widget background */
--scell-surface: #f8fafc; /* Card and input backgrounds */
--scell-surface-hover: #f1f5f9; /* Surface hover */
--scell-border: #e2e8f0; /* Borders */
--scell-border-focus: #818cf8; /* Input focus ring color */
--scell-text: #0f172a; /* Primary text */
--scell-text-secondary: #475569; /* Secondary text */
--scell-text-muted: #94a3b8; /* Muted/placeholder text */
--scell-success: #22c55e; /* Success color */
--scell-success-bg: #f0fdf4; /* Success background */
--scell-error: #ef4444; /* Error color */
--scell-error-bg: #fef2f2; /* Error background */
--scell-warning: #f59e0b; /* Warning color */
--scell-warning-bg: #fffbeb; /* Warning background */
--scell-info: #3b82f6; /* Info color */
--scell-info-bg: #eff6ff; /* Info background */
}All properties are optional. Unset properties fall back to the theme defaults (light or dark).
Example — brand color override:
scell-onboarding {
--scell-primary: #7c3aed;
--scell-primary-hover: #8b5cf6;
--scell-primary-active: #6d28d9;
--scell-border-focus: #8b5cf6;
}7.3 Host element sizing
The widget fills its host element. Control sizing with standard CSS:
scell-onboarding {
width: 100%;
max-width: 560px;
height: 680px;
border-radius: 12px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: block; /* Set automatically by the widget */
}Or use the HTML attributes:
<scell-onboarding width="560px" height="680px" ...></scell-onboarding>7.4 Internationalization (FR / EN)
Two locales are built-in. Set the locale attribute:
<scell-onboarding locale="en" ...></scell-onboarding>The locale affects all text in the widget: step labels, button labels, error messages, placeholders, and consent checkbox links.
To switch locale dynamically (e.g., based on a user preference toggle):
const widget = document.querySelector('scell-onboarding');
widget.setAttribute('locale', 'en');
// The widget re-renders the current step in the new localeNote: Locale switching re-renders the current step, which resets any form input the user had typed in that step. Do not switch locales mid-flow unless you provide explicit user control (a language selector).
8. Integration — React
Import the side-effect import to register the custom element, then use it as JSX.
// onboarding-widget-registration.ts (import once at app root)
import '@scell/onboarding-widget';// OnboardingPage.tsx
import { useEffect, useRef } from 'react';
import type {
OnboardingCompletedPayload,
OnboardingErrorPayload,
OnboardingStepPayload,
PopupOpenedPayload
} from '@scell/onboarding-widget';
interface OnboardingWidgetProps {
publishableKey: string;
externalId: string;
onComplete: (code: string) => void;
onError: (code: string, message: string) => void;
}
export function OnboardingWidget({
publishableKey,
externalId,
onComplete,
onError
}: OnboardingWidgetProps) {
const widgetRef = useRef<HTMLElement>(null);
useEffect(() => {
const el = widgetRef.current;
if (!el) return;
const handleComplete = (e: Event) => {
const { authorizationCode } = (e as CustomEvent<OnboardingCompletedPayload>).detail;
onComplete(authorizationCode);
};
const handleError = (e: Event) => {
const { code, message } = (e as CustomEvent<OnboardingErrorPayload>).detail;
onError(code, message);
};
const handleStep = (e: Event) => {
const { step, progress } = (e as CustomEvent<OnboardingStepPayload>).detail;
console.log(`Step: ${step}, Progress: ${progress}%`);
};
const handlePopupOpened = (e: Event) => {
const { authorizeUrl } = (e as CustomEvent<PopupOpenedPayload>).detail;
console.log('SuperPDP popup opened:', authorizeUrl);
};
el.addEventListener('onboarding:completed', handleComplete);
el.addEventListener('onboarding:error', handleError);
el.addEventListener('onboarding:step', handleStep);
el.addEventListener('onboarding:popup-opened', handlePopupOpened);
return () => {
el.removeEventListener('onboarding:completed', handleComplete);
el.removeEventListener('onboarding:error', handleError);
el.removeEventListener('onboarding:step', handleStep);
el.removeEventListener('onboarding:popup-opened', handlePopupOpened);
};
}, [onComplete, onError]);
return (
<scell-onboarding
ref={widgetRef}
publishable-key={publishableKey}
external-id={externalId}
theme="light"
locale="fr"
height="680px"
style={{ borderRadius: '12px', border: '1px solid #e2e8f0' }}
/>
);
}TypeScript JSX type declarations — add to a .d.ts file in your project:
// custom-elements.d.ts
import type { ScellOnboarding } from '@scell/onboarding-widget';
declare global {
namespace JSX {
interface IntrinsicElements {
'scell-onboarding': React.DetailedHTMLProps<
React.HTMLAttributes<ScellOnboarding> & {
'publishable-key'?: string;
'theme'?: 'light' | 'dark' | 'auto';
'locale'?: 'fr' | 'en';
'external-id'?: string;
'callback-url'?: string;
'width'?: string;
'height'?: string;
},
ScellOnboarding
>;
}
}
}9. Integration — Vue 3
<!-- OnboardingWidget.vue -->
<template>
<scell-onboarding
ref="widgetRef"
:publishable-key="publishableKey"
:external-id="externalId"
theme="auto"
locale="fr"
height="680px"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import '@scell/onboarding-widget';
import type {
OnboardingCompletedPayload,
OnboardingErrorPayload
} from '@scell/onboarding-widget';
const props = defineProps<{
publishableKey: string;
externalId: string;
}>();
const emit = defineEmits<{
complete: [code: string, externalId: string | undefined];
error: [code: string, message: string];
}>();
const widgetRef = ref<HTMLElement | null>(null);
function handleComplete(e: Event) {
const detail = (e as CustomEvent<OnboardingCompletedPayload>).detail;
emit('complete', detail.authorizationCode, detail.externalId);
}
function handleError(e: Event) {
const detail = (e as CustomEvent<OnboardingErrorPayload>).detail;
emit('error', detail.code, detail.message);
}
onMounted(() => {
const el = widgetRef.value;
if (!el) return;
el.addEventListener('onboarding:completed', handleComplete);
el.addEventListener('onboarding:error', handleError);
});
onBeforeUnmount(() => {
const el = widgetRef.value;
if (!el) return;
el.removeEventListener('onboarding:completed', handleComplete);
el.removeEventListener('onboarding:error', handleError);
});
</script>Vite config — tell Vue to treat scell-* tags as custom elements:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('scell-')
}
}
})
]
});10. Integration — Vanilla JS
Complete self-contained example:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Onboarding Client</title>
<style>
body {
font-family: system-ui, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 40px 20px;
background: #f5f5f5;
box-sizing: border-box;
}
.widget-wrapper {
width: 100%;
max-width: 540px;
}
scell-onboarding {
border: 1px solid #d9d9d9;
border-radius: 8px;
overflow: hidden;
/* Brand color overrides */
--scell-primary: #7c3aed;
--scell-primary-hover: #8b5cf6;
--scell-primary-active: #6d28d9;
--scell-border-focus: #8b5cf6;
}
#status-message {
margin-top: 16px;
padding: 12px;
border-radius: 6px;
font-size: 14px;
display: none;
}
#status-message.success {
background: #f0fdf4;
color: #166534;
border: 1px solid #22c55e;
}
#status-message.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #ef4444;
}
</style>
</head>
<body>
<div class="widget-wrapper">
<scell-onboarding
id="onboarding"
publishable-key="pk_test_YOUR_KEY_HERE"
theme="light"
locale="fr"
external-id="customer_42"
height="680px"
></scell-onboarding>
<div id="status-message"></div>
</div>
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>
<script>
const widget = document.getElementById('onboarding');
const statusMessage = document.getElementById('status-message');
function showStatus(message, type) {
statusMessage.textContent = message;
statusMessage.className = type;
statusMessage.style.display = 'block';
}
// Session start — useful for logging or storing the session ID
widget.addEventListener('onboarding:started', (e) => {
console.log('Session started:', e.detail.sessionId);
});
// Step navigation — update a custom progress indicator if needed
widget.addEventListener('onboarding:step', (e) => {
console.log(`Step: ${e.detail.step} (${e.detail.progress}% complete)`);
});
// SuperPDP popup opened — optional tracking
widget.addEventListener('onboarding:popup-opened', (e) => {
console.log('SuperPDP popup opened:', e.detail.authorizeUrl);
});
// Onboarding completed — critical handler
widget.addEventListener('onboarding:completed', async (e) => {
const { authorizationCode, externalId } = e.detail;
try {
// Exchange the code via YOUR backend
// NEVER call the Scell Partner API directly from the browser
const response = await fetch('/api/onboarding/finalize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: authorizationCode,
customerId: externalId
})
});
if (!response.ok) throw new Error('Exchange failed');
showStatus('Inscription reussie ! Votre compte Scell est actif.', 'success');
} catch (err) {
showStatus('Erreur lors de la finalisation. Contactez le support.', 'error');
console.error('Exchange error:', err);
}
});
// Errors — display a user-facing message
widget.addEventListener('onboarding:error', (e) => {
const { code, message } = e.detail;
console.error(`Widget error [${code}]: ${message}`);
if (code === 'INVALID_PUBLISHABLE_KEY') {
showStatus('Configuration incorrecte. Contactez votre prestataire.', 'error');
} else {
showStatus('Une erreur est survenue. Veuillez recharger la page.', 'error');
}
});
</script>
</body>
</html>11. Public API Reference
The <scell-onboarding> element exposes the following JavaScript methods in addition to the standard HTMLElement API.
reset(): void
Resets the widget to its initial state: clears the current step back to connect, closes any open SuperPDP popup, destroys the current API session reference, and re-renders.
Use this to allow the same widget instance to start a fresh onboarding flow.
const widget = document.querySelector('scell-onboarding');
widget.reset();Note: A new API session is created automatically when the widget re-renders after reset().
getCurrentStep(): OnboardingStep
Returns the current step key as a string.
Return type: 'connect' | 'redirect' | 'complete'
const currentStep = widget.getCurrentStep();
// e.g. "documents"getProgress(): number
Returns the current progress as a number between 0 and 100.
| Step | Value |
|---|---|
connect | 0 |
redirect | 50 |
complete | 100 |
const progress = widget.getProgress();
// e.g. 50Inherited attributes (HTMLElement)
Since <scell-onboarding> is a standard custom element, you can also use:
widget.setAttribute('theme', 'dark')— changes theme and triggers re-renderwidget.getAttribute('locale')— reads current localewidget.removeAttribute('callback-url')— removes optional config- All standard DOM methods (
addEventListener,dispatchEvent, etc.)
12. Troubleshooting
"Publishable key is required" shown in the widget
The publishable-key attribute is missing or empty. Ensure you set it before the element connects to the DOM.
<!-- Wrong -->
<scell-onboarding></scell-onboarding>
<!-- Correct -->
<scell-onboarding publishable-key="pk_test_abc123"></scell-onboarding>onboarding:error fires with code INIT_ERROR immediately
The session creation call to POST /v1/onboarding/sessions failed. Common causes:
- Invalid
publishable-key: Verify the key in your Scell Partner dashboard. Sandbox keys start withpk_test_, production keys withpk_live_. The environment is determined automatically from the key prefix. - CORS: If you are running on
localhostand your browser blocks the request, check that your Scell partner account haslocalhostwhitelisted in allowed origins (sandbox only). - Network issue: Check the browser Network panel for the failing request and inspect the response body.
The widget is invisible / has zero height
The custom element is a replaced element with display: block set by the widget. However, if its parent has height: 0 or overflow: hidden, the widget collapses. Also verify that the height attribute (or CSS height) is set:
<scell-onboarding height="650px" ...></scell-onboarding>Shadow DOM styles conflict with my page
They should not — the Shadow DOM isolates the widget's styles. If you see styling issues, check that you are not using !important rules targeting scell-onboarding * from your page CSS (which cannot pierce Shadow DOM) or that you are not using a CSS reset that targets the host element itself (scell-onboarding { ... }). Only host-level rules apply; inner element rules do not.
CSS custom properties are not applied
Custom properties set on scell-onboarding from the host page do pierce Shadow DOM — this is the intended mechanism. Verify:
- The property name is spelled exactly as documented (prefix
--scell-). - The rule targets
scell-onboarding(the element itself), not a class inside it.
/* Correct — targets the host element */
scell-onboarding {
--scell-primary: #7c3aed;
}
/* Wrong — .scell-btn is inside Shadow DOM and cannot be targeted from outside */
scell-onboarding .scell-btn {
background: #7c3aed;
}onboarding:completed fires but exchanging the code fails
The authorizationCode is a one-time, short-lived token. Do not:
- Store it in
localStorageorsessionStoragebefore exchanging it - Delay the exchange by more than a few seconds
- Call the exchange endpoint more than once with the same code
If the exchange fails, the user must restart the onboarding. There is no way to re-issue the authorization code.
The widget does not resume after page refresh
Session resumption is supported by the API: if the same publishable-key and external-id combination previously created a session that is still active (not expired), the API returns the saved step.
If the widget always starts from step 1 (connect) after a refresh, check that:
- The
external-idattribute is set consistently (same value as the previous load). - The session has not expired (sessions expire after a fixed TTL; check your Scell Partner dashboard for the configured value).
Note: because the SuperPDP flow runs in a popup, the main page can be refreshed without losing the session — the popup closes independently.
The SuperPDP popup is blocked by the browser
Browsers block popups that are not triggered directly by a user gesture. The widget opens the popup inside the button click handler, which satisfies most browsers. If the popup is still blocked:
- Do not wrap the
<scell-onboarding>element inside an<iframe>— cross-origin iframes cannot open popups. - Verify that no browser extension (ad blocker, privacy extension) is suppressing the popup.
- As a fallback, the widget shows a direct link to the authorization URL when
window.openreturnsnull.
The widget stays on the redirect step indefinitely
The widget is waiting for a postMessage from the SuperPDP callback page. Common causes:
- Callback URL mismatch: The redirect URI registered in your SuperPDP developer account must exactly match the Scell backend callback URL (
https://api.scell.io/api/v1/onboarding/superpdp/callback). A mismatch causes SuperPDP to show an error instead of redirecting. - Popup closed before completing: The user closed the popup manually. The "Réouvrir la fenêtre" link reopens it.
postMessageorigin blocked by CSP: Check your page'sContent-Security-Policy— it must not blockframe-ancestorsor restrict message sources fromapi.scell.io.- Network error during callback exchange: Check
POST /api/v1/onboarding/superpdp/callbackin the Network panel of the popup window's DevTools (open DevTools before the popup appears, or use--remote-debugging-port).
TypeScript: "Property 'scell-onboarding' does not exist on type 'JSX.IntrinsicElements'"
Add the JSX type declarations described in section 8 to a .d.ts file included in your tsconfig.json. Alternatively, use React.createElement without JSX type checking for a quick workaround:
// Quick workaround (not recommended long-term)
const ScellWidget = 'scell-onboarding' as unknown as React.ComponentType<Record<string, string>>;
<ScellWidget publishable-key="pk_test_..." />Sandbox testing
In sandbox mode (using a pk_test_* key), the SuperPDP popup connects to the SuperPDP sandbox environment. Use the SuperPDP sandbox test credentials to simulate a successful KYB flow without real company data.
The authorization code returned at the end is a sandbox code. Exchange it with POST /v1/onboarding/exchange against https://api.scell.io/api/v1 — this will create a sandbox tenant (DB rdb_sandbox).
SuperPDP sandbox test credentials are managed in your SuperPDP developer account. Contact Scell support if you need a pre-configured sandbox environment for integration testing.