Skip to content

Scell Onboarding Widget — Developer Documentation

Package: @scell/onboarding-widget v1.1.0 Custom element: <scell-onboarding> Build output: ES module + IIFE (UMD CJS also exported)


Table of Contents

  1. Overview
  2. Installation
  3. Quick Start
  4. Configuration
  5. Onboarding Flow — The 4 Steps
  6. Events and Callbacks
  7. Customization
  8. Integration — React
  9. Integration — Vue 3
  10. Integration — Vanilla JS
  11. Public API Reference
  12. 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 4-step KYB (Know Your Business) onboarding
  • Verifies the company SIRET number against the French company registry (INSEE/Sirene)
  • Optionally validates the EU intra-community VAT number (VIES)
  • Collects and uploads KYB compliance documents (Kbis, ID card, proof of address, bank details)
  • Captures legal representative information and consent
  • Returns an authorization_code to 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 and receive a callback with the authorization code. The partner exchanges that code for API credentials using POST /v1/onboarding/exchange-code.

Technical design

  • Implemented as a native Custom Element (HTMLElement subclass)
  • 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, headers X-Publishable-Key and X-Session-Id
  • Emits standard CustomEvent instances that bubble up through the DOM
  • Supports light, dark, and auto (OS preference) themes via CSS custom properties exposed on :host
  • Fully responsive, tested down to 320 px viewport width

2. Installation

npm / pnpm / yarn

bash
npm install @scell/onboarding-widget
# or
pnpm add @scell/onboarding-widget
# or
yarn add @scell/onboarding-widget

The package ships pre-built in dist/. No build step required on the consumer side.

CDN (script tag — IIFE build)

html
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>

Or pin to a specific version:

html
<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)

html
<script type="module">
  import '@scell/onboarding-widget';
</script>

Or from a CDN supporting ESM:

html
<script type="module" src="https://cdn.scell.io/widget/v1/onboarding.esm.js"></script>

3. Quick Start

Minimum viable integration — 5 lines of HTML:

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. It connects to the sandbox environment, shows labels in French, and uses the light theme.


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

AttributeTypeDefaultRequiredDescription
publishable-keystringYesYour Scell publishable key (pk_live_... or pk_test_...). The widget will not initialize without this.
environment"sandbox" | "production""sandbox"NoDetermines the API base URL. Sandbox: https://api-sandbox.scell.io/v1. Production: https://api.scell.io/v1.
theme"light" | "dark" | "auto""light"NoColor scheme. "auto" respects the OS-level prefers-color-scheme media query.
locale"fr" | "en""fr"NoInterface language. All labels, error messages, and placeholders are translated.
external-idstringNoYour 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-urlstringNoIf 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.
widthstring"100%"NoCSS width of the host element (any valid CSS length: 480px, 100%, 50vw…).
heightstring"600px"NoCSS height of the host element. The inner content scrolls if it overflows. 650px is recommended for the documents step.

HTML example with all options

html
<scell-onboarding
  publishable-key="pk_live_a1b2c3d4e5f6"
  environment="production"
  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

javascript
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 4 Steps

The widget walks users through a linear, guided flow. Navigation is sequential: each step must be completed before proceeding. Users can navigate back to previous steps.

Progress is tracked by the API: if the user refreshes or reopens the widget with the same session, the API will resume from the saved step.

Step 1 — SIRET (siret)

Purpose: Identify the company.

What the user does:

  • Enters a 14-digit SIRET number
  • The input auto-formats to XXX XXX XXX XXXXX as the user types
  • Clicks "Vérifier" / "Verify"

What happens behind the scenes:

  • POST /v1/onboarding/verify-company with { siret } in the body
  • On success: the API returns company data (name, SIREN, legal form, address); the widget transitions to step 2
  • On SIRET_NOT_FOUND: inline error below the field
  • On network error: generic retry message

Validation:

  • Exactly 14 digits required (non-digit characters are stripped before validation)
  • Client-side check runs before the API call

Layout:

+----------------------------------+
|  [scell.io logo]                 |
|  [1] SIRET > [2] > [3] > [4]    |
|                                  |
|  Identifiez votre entreprise     |
|  Entrez le numero SIRET...       |
|                                  |
|  SIRET                           |
|  [ ___  ___  ___  _____ ]        |
|  < error message if any >        |
|                                  |
|  [ Verifier ]                    |
+----------------------------------+

Step 2 — Verification (verify)

Purpose: Confirm company data and optionally validate the EU VAT number.

What the user sees:

  • A read-only card displaying: raison sociale, SIRET (formatted), SIREN, legal form (forme juridique), and address retrieved from the registry
  • An optional input for the EU intra-community VAT number (e.g., FR12345678901)
  • "Vérifier la TVA" button that calls POST /v1/onboarding/verify-vat
  • "Passer cette étape" / "Skip this step" link (VAT is optional)
  • Back / Continue buttons

What happens behind the scenes:

  • VAT verification calls POST /v1/onboarding/verify-vat with { vat_number }
  • On valid VAT: success alert inline, VAT stored in session
  • Clicking "Continue" without verifying VAT is allowed

Layout:

+----------------------------------+
|  [1 done] > [2 ACTIVE] > [3] > [4] |
|                                  |
|  Confirmez les informations      |
|                                  |
|  +-- Informations de l'ent. ----+|
|  | Raison sociale   ACME SAS   ||
|  | SIRET            123 456... ||
|  | SIREN            123 456 789||
|  | Forme juridique  SAS        ||
|  | Adresse          12 rue...  ||
|  +------------------------------+|
|                                  |
|  TVA intracommunautaire (opt.)   |
|  [ FR____________ ] [Verifier]   |
|  < success/error status >        |
|                                  |
|  [ Retour ]     [ Continuer ]    |
+----------------------------------+

Step 3 — Documents (documents)

Purpose: Collect KYB compliance documents.

Document types:

Type keyLabel (FR)Required
kbisExtrait Kbis (moins de 3 mois)Yes
id_cardPièce d'identité du représentant légalYes
proof_of_addressJustificatif de domicileNo
bank_detailsRIB / IBANNo

File constraints:

  • Accepted formats: PDF, JPEG, PNG
  • Maximum size: 10 MB per file

What happens:

  • Clicking the upload button triggers a hidden <input type="file">
  • POST /v1/onboarding/documents with FormData containing type and file
  • On success: the document appears in the list with a "En attente" (pending) badge
  • Documents can be replaced at any time before proceeding
  • Clicking "Continue" with missing required documents highlights the incomplete items in red and blocks navigation

Layout:

+----------------------------------+
|  [1] > [2] > [3 ACTIVE] > [4]  |
|                                  |
|  Documents justificatifs         |
|                                  |
|  DOCUMENTS OBLIGATOIRES          |
|  +-- Extrait Kbis -----------+  |
|  | kbis_acme.pdf  [En att.]  |  |
|  |              [Remplacer]  |  |
|  +---------------------------+  |
|  +-- Piece d'identite -------+  |
|  |              [Choisir]    |  |
|  +---------------------------+  |
|                                  |
|  DOCUMENTS OPTIONNELS            |
|  +-- Justificatif domicile --+  |
|  ...                            |
|                                  |
|  [ Retour ]     [ Continuer ]    |
+----------------------------------+

Step 4 — Complete (complete)

Purpose: Capture legal representative details and final consent.

Form fields:

FieldTypeRequired
First name (firstName)textYes
Last name (lastName)textYes
Email (email)emailYes
Phone (phone)telNo
Role / Position (role)selectYes
Accept Terms & ConditionscheckboxYes
Accept Privacy PolicycheckboxYes

Role options: Dirigeant / CEO, Directeur financier / CFO, Directeur technique / CTO, Manager, Autre.

Pre-fill: If the API session already carries representative data, the form fields are pre-populated.

On submission:

  • POST /v1/onboarding/complete with session ID, representative object, and consent flags
  • On success: the form is replaced by a success message; the onboarding:completed event fires with the authorizationCode; if callback-url is set, redirection happens after 2 seconds

On error:

  • The submit button reverts; an inline error alert displays the message from the API

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

javascript
const widget = document.querySelector('scell-onboarding');

widget.addEventListener('onboarding:completed', (event) => {
  console.log(event.detail.authorizationCode);
});

Or at the document level (events bubble):

javascript
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.

typescript
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.

javascript
widget.addEventListener('onboarding:started', (e) => {
  localStorage.setItem('scell_session_id', e.detail.sessionId);
});

onboarding:step

Fired every time the user navigates to a different step (including back navigation).

typescript
interface OnboardingStepPayload {
  step: 'siret' | 'verify' | 'documents' | 'complete';
  progress: number; // 0, 25, 50, or 75
}

Note: progress is 75 when the user reaches the complete step. The value becomes implicitly 100 only when onboarding:completed fires.

javascript
widget.addEventListener('onboarding:step', (e) => {
  updateProgressBar(e.detail.progress);
});

onboarding:siret-verified

Fired when the SIRET validation succeeds and the company data is retrieved.

typescript
interface SiretVerifiedPayload {
  companyName: string;
  siret: string; // 14-digit string, no spaces
}
javascript
widget.addEventListener('onboarding:siret-verified', (e) => {
  console.log(`Company identified: ${e.detail.companyName}`);
});

onboarding:documents-uploaded

Fired each time a document is successfully uploaded (not when the step is completed, but after each individual file upload).

typescript
interface DocumentsUploadedPayload {
  count: number; // Total number of documents uploaded so far in this session
}

onboarding:completed

Fired after a successful final submission. This is the primary event your integration must handle.

typescript
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-code as soon as possible. Do not expose it in browser storage.

javascript
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.

typescript
interface OnboardingErrorPayload {
  code: string;    // Machine-readable error code, e.g. "INIT_ERROR", "INVALID_PARTNER_KEY"
  message: string; // Human-readable description
}
javascript
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:

typescript
import type {
  OnboardingStartedPayload,
  OnboardingStepPayload,
  SiretVerifiedPayload,
  DocumentsUploadedPayload,
  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:siret-verified': CustomEvent<SiretVerifiedPayload>;
    'onboarding:documents-uploaded': CustomEvent<DocumentsUploadedPayload>;
    'onboarding:completed': CustomEvent<OnboardingCompletedPayload>;
    'onboarding:error': CustomEvent<OnboardingErrorPayload>;
  }
}

7. Customization

7.1 Theme — light, dark, auto

Set via the theme attribute:

html
<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:

css
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:

css
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:

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:

html
<scell-onboarding width="560px" height="680px" ...></scell-onboarding>

7.4 Internationalization (FR / EN)

Two locales are built-in. Set the locale attribute:

html
<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):

javascript
const widget = document.querySelector('scell-onboarding');
widget.setAttribute('locale', 'en');
// The widget re-renders the current step in the new locale

Note: 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.

typescript
// onboarding-widget-registration.ts (import once at app root)
import '@scell/onboarding-widget';
tsx
// OnboardingPage.tsx
import { useEffect, useRef } from 'react';
import type {
  OnboardingCompletedPayload,
  OnboardingErrorPayload,
  OnboardingStepPayload
} 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}%`);
    };

    el.addEventListener('onboarding:completed', handleComplete);
    el.addEventListener('onboarding:error', handleError);
    el.addEventListener('onboarding:step', handleStep);

    return () => {
      el.removeEventListener('onboarding:completed', handleComplete);
      el.removeEventListener('onboarding:error', handleError);
      el.removeEventListener('onboarding:step', handleStep);
    };
  }, [onComplete, onError]);

  return (
    <scell-onboarding
      ref={widgetRef}
      publishable-key={publishableKey}
      external-id={externalId}
      environment="production"
      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:

typescript
// 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;
          'environment'?: 'sandbox' | 'production';
          'theme'?: 'light' | 'dark' | 'auto';
          'locale'?: 'fr' | 'en';
          'external-id'?: string;
          'callback-url'?: string;
          'width'?: string;
          'height'?: string;
        },
        ScellOnboarding
      >;
    }
  }
}

9. Integration — Vue 3

vue
<!-- OnboardingWidget.vue -->
<template>
  <scell-onboarding
    ref="widgetRef"
    :publishable-key="publishableKey"
    :external-id="externalId"
    environment="production"
    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:

typescript
// 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:

html
<!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"
      environment="sandbox"
      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)`);
    });

    // SIRET verified — you know which company is onboarding
    widget.addEventListener('onboarding:siret-verified', (e) => {
      console.log(`Company: ${e.detail.companyName} (SIRET: ${e.detail.siret})`);
    });

    // Document uploaded — optional tracking
    widget.addEventListener('onboarding:documents-uploaded', (e) => {
      console.log(`Documents uploaded so far: ${e.detail.count}`);
    });

    // 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 siret, discards any collected company data and documents, destroys the current API session reference, and re-renders.

Use this to allow the same widget instance to start a fresh onboarding flow.

javascript
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: 'siret' | 'verify' | 'documents' | 'complete'

javascript
const currentStep = widget.getCurrentStep();
// e.g. "documents"

getProgress(): number

Returns the current progress as a number between 0 and 75.

StepValue
siret0
verify25
documents50
complete75

Progress reaches 100 implicitly when onboarding:completed fires (no separate method).

javascript
const progress = widget.getProgress();
// e.g. 50

Inherited attributes (HTMLElement)

Since <scell-onboarding> is a standard custom element, you can also use:

  • widget.setAttribute('theme', 'dark') — changes theme and triggers re-render
  • widget.getAttribute('locale') — reads current locale
  • widget.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.

html
<!-- 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 with pk_test_, production keys with pk_live_.
  • Wrong environment: Using a production key with environment="sandbox" (or vice versa) causes authentication failure.
  • CORS: If you are running on localhost and your browser blocks the request, check that your Scell partner account has localhost whitelisted 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:

html
<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:

  1. The property name is spelled exactly as documented (prefix --scell-).
  2. The rule targets scell-onboarding (the element itself), not a class inside it.
css
/* 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 localStorage or sessionStorage before 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 and company data.

If the widget always starts from step 1 after a refresh, check that:

  1. The external-id attribute is set consistently (same value as the previous load).
  2. The session has not expired (sessions expire after a fixed TTL; check your Scell Partner dashboard for the configured value).

The widget shows a perpetual spinner after SIRET entry

The SIRET verification call is pending or has silently failed. Check:

  • Browser Network panel for POST /v1/onboarding/verify-company
  • Whether the request is blocked by an ad blocker or browser extension (the Scell sandbox domain may be flagged)
  • Whether the SIRET entered is a valid, active French SIRET (closed or dissolved companies may return SIRET_NOT_FOUND)

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:

tsx
// 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 — bypassing real document verification

In sandbox mode (environment="sandbox"), the API accepts any file for document upload without real validation. You can upload dummy PDFs. The authorization code returned at the end is also a sandbox code — use it with POST /v1/onboarding/exchange-code against https://api-sandbox.scell.io/v1.

SIRET verification in sandbox mode is connected to a test dataset. Use the following test SIRETs:

SIRETCompanyNotes
12345678900012ACME TEST SASValid, active
99999999900001TEST CORP SARLValid, active, no VAT
00000000000000Always returns SIRET_NOT_FOUND

Documentation Scell.io