Skip to main content
Developer Documentation

Developer documentation

Complete guide to the Scell.io API: electronic invoicing (Factur-X / UBL / CII), intra-EU VAT & reverse charge, eIDAS signatures, quotes, credit notes, recurring invoices, ISCA fiscal compliance — plus the no-code onboarding widget. curl and SDK examples (PHP, JS, MCP).

Install in 30 seconds
index.html
html
<!-- 1. Charger le script (un seul include, n'importe ou dans <head> ou <body>) -->
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>

<!-- 2. Poser le widget la ou il doit s'afficher -->
<scell-onboarding
  publishable-key="pk_test_xxxxxxxx"
  external-id="user_42"
  callback-url="https://votre-app.com/onboarding/done"
></scell-onboarding>

<!-- 3. Ecouter l'evenement de fin pour recuperer sub_tenant + credentials -->
<script>
  document.querySelector('scell-onboarding')
    .addEventListener('onboarding:completed', (e) => {
      const { subTenant, credentials } = e.detail;
      console.log('Sub-tenant cree :', subTenant.id, subTenant.name);
      console.log('A utiliser pour facturer :', credentials);
    });
</script>

Introduction

Scell.io is an API platform for electronic invoicing (Factur-X / UBL / CII, EN16931-compliant and aligned with the French e-invoicing reform) and simple electronic signatures (eIDAS EU-SES). This guide covers the entire API by functional domain, with curl and SDK examples. For the exhaustive endpoint-by-endpoint reference, see the OpenAPI reference.

Invoicing

Factur-X / UBL / CII, SUPER PDP / PEPPOL transmission, lifecycle, credit notes, quotes, recurring.

eIDAS signatures

Simple electronic signatures via OpenAPI.com, multi-signer, OTP, time-stamped proof.

Fiscal compliance

Immutable ISCA chain (SHA-256), closings, OpenTimestamps anchoring, FEC export.

Base URL & environments

A single host for production AND sandbox. The API key prefix selects the environment (and the isolated database).

bash
# Base URL unique (production ET sandbox)
https://api.scell.io/api/v1

# Le préfixe de la clé détermine l'environnement automatiquement :
#   sk_live_… / pk_live_…  -> base de données production
#   sk_test_… / pk_test_…  -> base de données sandbox (données de test isolées)
# Il n'existe PAS de host api-sandbox.scell.io — tout passe par api.scell.io.

Authentication

Four authentication modes depending on context. Golden rule: secret keys sk_* are SERVER-SIDE only (never in a browser or mobile app).

Key / modeHeaderUsage
sk_live_* / sk_test_*X-API-KeyServer-side. Issue invoices, credit notes, signatures, webhooks. Full tenant scope.
pk_live_* / pk_test_*X-Publishable-KeyFront / widget. Public widget endpoints only. Safe in the browser.
Bearer (Sanctum)Authorization: BearerDashboard SPA app.scell.io (HttpOnly cookies) or API clients.
X-Tenant-Key (legacy)X-Tenant-KeyLegacy multi-tenant. Prefer sk_*/pk_* for new projects.
The 4 modes
bash
# 1) Secret key (server-side uniquement) — émission factures, signatures, avoirs
curl https://api.scell.io/api/v1/invoices \
  -H 'X-API-Key: sk_live_xxxxxxxx'

# 2) Publishable key (front/widget) — endpoints widget publics uniquement
curl -X POST https://api.scell.io/api/v1/widget/onboarding/sirene/lookup \
  -H 'X-Publishable-Key: pk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{ "siret": "12345678901234" }'

# 3) Bearer (dashboard SPA, Sanctum) — cookies HttpOnly côté app.scell.io
curl https://api.scell.io/api/v1/auth/me -H 'Authorization: Bearer <token>'

# 4) X-Tenant-Key (legacy multi-tenant) — préférer sk_*/pk_* pour tout nouveau projet

Create an account & keys

bash
# Créer un compte (tenant) — retourne la 1re clé API
curl -X POST https://api.scell.io/api/v1/register \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "ACME SAS",
    "email": "dev@acme.fr",
    "password": "••••••••••••",
    "password_confirmation": "••••••••••••"
  }'

# Émettre une clé secrète server-side supplémentaire (Bearer requis)
curl -X POST https://api.scell.io/api/v1/auth/me/api-key/rotate -H 'Authorization: Bearer <token>'

Key concepts

Multi-tenant model

  • TenantThe master account (yours). The sk_* key belongs to the tenant.
  • Sub-tenantAn end client you invoice white-label (created via the onboarding widget). Target it via "sub_tenant_id" in the request body.
  • CompanyThe issuing entity (SIRET, IBAN, Factur-X mentions). Resolved automatically by Scell.io.
  • Buyer / SupplierRegistries scoped by (tenant, sub_tenant). Buyers: created manually or upserted from invoice. Suppliers: auto-derived from received invoices — no manual create, enrichment of email/phone/notes/metadata only.
  • ProductReusable product/service catalogue scoped by (tenant, sub_tenant). Pre-fills a line via "product_id"; the "save_to_catalog" flag stores a typed line. Fiscal categorisation (revenue_category) + custom categories.
Simplified model: the sk_* key = the tenant. Without "sub_tenant_id", the action is on behalf of the master tenant. With "sub_tenant_id", it is scoped to that sub-tenant (404 SUB_TENANT_NOT_FOUND if it does not belong to your tenant — anti-IDOR). No "company_id" to provide: the issuer is resolved automatically.

Numbering & immutability

Never pass "invoice_number": Scell.io generates it. On creation the invoice is a draft (DRAFT-… number); on submit it receives a definitive gap-free sequential number (French CGI art. 242 nonies A & 289) and becomes immutable (ISCA SHA-256 chain). Any correction goes through a credit note.

Electronic invoicing (Factur-X)

Issue Factur-X (PDF/A-3 + embedded EN16931 XML), UBL or CII compliant invoices. Lifecycle: create (draft) → submit (seal the number + transmit via SUPER PDP / PEPPOL) → track (transmitted, accepted, rejected, paid).

bash
# Créer une facture Factur-X (B2B FR). invoice_number est généré par Scell.io.
curl -X POST https://api.scell.io/api/v1/invoices \
  -H 'X-API-Key: sk_live_xxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "direction": "outgoing",
    "output_format": "facturx",            // facturx | ubl | cii
    "issue_date": "2026-06-05",
    "currency": "EUR",
    "seller_siret": "12345678901234",
    "seller_name": "ACME SAS",
    "seller_address": { "line1": "1 rue de Paris", "postal_code": "75001", "city": "Paris", "country": "FR" },
    "buyer_name": "Client SARL",
    "buyer_siret": "98765432109876",
    "buyer_address": { "line1": "2 av. Lyon", "postal_code": "69001", "city": "Lyon", "country": "FR" },
    "lines": [
      { "description": "Prestation de conseil", "quantity": 1, "unit_price": 1000, "tax_rate": 20,
        "total_ht": 1000, "total_tax": 200, "total_ttc": 1200 }
    ]
  }'

# Réponse 201 — la facture est en brouillon (status="draft", numéro "DRAFT-…")
# { "data": { "id": "019df46f-…", "invoice_number": "DRAFT-2026-0001", "status": "draft", … } }

# Émettre (scelle le numéro définitif + transmet à SUPER PDP / PEPPOL)
curl -X POST https://api.scell.io/api/v1/invoices/{id}/submit -H 'X-API-Key: sk_live_xxxxxxxx'

# Télécharger le Factur-X (PDF/A-3 + XML embarqué) ou le XML seul
curl https://api.scell.io/api/v1/invoices/{id}/download/pdf -H 'X-API-Key: sk_live_xxxxxxxx' -o facture.pdf
curl https://api.scell.io/api/v1/invoices/{id}/download/xml -H 'X-API-Key: sk_live_xxxxxxxx' -o facture.xml

Lifecycle & endpoints

ActionEndpointEffect
CreatePOST /invoicesDraft, DRAFT-… number
UpdatePUT /invoices/{id}While draft
SubmitPOST /invoices/{id}/submitSeal number + transmit (immutable)
Mark paidPOST /invoices/{id}/mark-paidBT-81 payment means required
DownloadGET /invoices/{id}/download/{pdf|xml}Factur-X / XML
Audit trailGET /invoices/{id}/audit-trailTime-stamped history
BulkPOST /invoices/bulk-submit · bulk-statusMass processing

B2C (individual)

bash
# B2C (particulier) : buyer_is_individual=true -> SIRET/TVA acheteur optionnels,
# mentions L441-10 (pénalités) et BT-46/47/48 omises (BR-CO-26 EN16931).
curl -X POST https://api.scell.io/api/v1/invoices \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "direction": "outgoing", "output_format": "facturx", "issue_date": "2026-06-05",
    "buyer_is_individual": true,
    "buyer_name": "Jean Dupont",
    "buyer_address": { "line1": "5 rue Bleue", "postal_code": "13001", "city": "Marseille", "country": "FR" },
    "lines": [ { "description": "Abonnement", "quantity": 1, "unit_price": 50, "tax_rate": 20 } ]
  }'

Related: incoming invoices (GET /tenant/invoices/incoming, accept/reject/dispute/mark-paid), invoice templates (/invoice-templates, including POST /invoice-templates/derive-colors-from-email-logo and the is_enabled field), live non-persisted HTML preview (POST /documents/preview), deposits & balances (see Quotes). For intra-EU VAT and reverse charge, see the next section.

VAT & intra-EU reverse charge

Scell.io resolves VAT AUTHORITATIVELY: on submit, the server recomputes each line’s expected EN16931 category from the context (seller, buyer, country, VIES validity of the VAT number, goods/services nature) and emits the code + exact legal mention in the Factur-X. This protects against tax reassessment (joint liability if 0% is applied without checks).

The 12 VAT categories

CategoryEN16931FR rateLegal mention (CGI)
STANDARDS20 %— (art. 278)
INTERMEDIATES10 %
REDUCEDS5,5 %
SUPER_REDUCEDS2,1 %
ZERO_RATEDZ0 %— (no BT-120 mention)
EXEMPTE0 %Exempt — art. 261
REVERSE_CHARGEAE0 %Reverse charge — art. 283-2
OUT_OF_SCOPEO0 %Out of scope — art. 259-1
INTRACOM_GOODSK0 %Intra-EU supply of goods — art. 262 ter, I
EXPORTG0 %Export of goods — art. 262, I
FRANCHISE_BASEE0 %Small-business franchise — art. 293 B
EXEMPT_TRAININGE0 %Vocational training — art. 261-4-4°a

The mention is NOT derived from the EN16931 code alone: FRANCHISE_BASE, EXEMPT and EXEMPT_TRAINING share code E but carry distinct mentions (293 B / 261 / 261-4-4°a). The applicative category is the source of truth for the mention.

Goods vs services — the supply_type field

Destinationsupply_type='services'supply_type='goods'
FR → FRSTANDARD (S)STANDARD (S)
FR → EU B2B (valid VAT)REVERSE_CHARGE (AE)INTRACOM_GOODS (K)
FR → non-EUOUT_OF_SCOPE (O)EXPORT (G)
FR → EU B2C / no VATFR VAT (art. 259-2)FR VAT

A line defaults to a service. Always set supply_type="goods" for a cross-border sale of physical goods.

VAT control fields (per line)

FieldDescription
vat_categoryExplicit category (otherwise server-resolved).
supply_type'goods' | 'services' — discriminates K/G vs AE/O.
place_of_supplyISO-2 country of place of supply (art. 259-A override).
vat_override_reasonAssume a divergent rate (avoids the 409, fiscal trail).

Examples

bash
# Prestation de SERVICES intra-UE B2B (numéro TVA valide VIES)
#   -> REVERSE_CHARGE (AE), TVA 0 %, mention « Autoliquidation - Article 283-2 du CGI »
curl -X POST https://api.scell.io/api/v1/invoices \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "direction": "outgoing", "output_format": "facturx", "issue_date": "2026-06-05",
    "seller_siret": "12345678901234", "seller_name": "ACME SAS",
    "buyer_name": "Müller GmbH", "buyer_country": "DE", "buyer_vat_number": "DE123456789",
    "buyer_address": { "line1": "Hauptstr. 1", "postal_code": "10115", "city": "Berlin", "country": "DE" },
    "lines": [
      { "description": "Conseil SaaS", "quantity": 1, "unit_price": 1000, "tax_rate": 0,
        "vat_category": "REVERSE_CHARGE", "supply_type": "services" }
    ]
  }'

VIES check & 409 response

Reverse charge (0%) is valid only if the buyer’s VAT number is ACTIVE in VIES. Negative cases handled: well-formed but non-existent number (DE999999999) → VIES failure → falls back to French VAT; EU client with no VAT number → no reverse charge, FR VAT; the country↔mention mapping is never hard-coded (IT, ES, etc. behave identically).
409 VAT_CORRECTION_REQUIRED
bash
# Le serveur RE-RÉSOUT la TVA de chaque ligne (résolution autoritaire).
# Si le taux est incohérent (ex. 20 % sur une vente intra-UE B2B avec n° TVA valide)
# ET qu'aucune raison d'override n'est fournie -> 409, facture NON persistée :
HTTP/1.1 409 Conflict
{
  "error": "VAT_CORRECTION_REQUIRED",
  "message": "Le taux de TVA d'une ou plusieurs lignes est incohérent avec le contexte…",
  "corrections": [
    {
      "line_index": 0,
      "provided_rate": 20,
      "suggested_rate": 0,
      "suggested_category": "REVERSE_CHARGE",
      "en16931_code": "AE",
      "mention": "Autoliquidation - Article 283-2 du CGI",
      "rule": "R2_eu_b2b_vat_valid"
    }
  ],
  "hint": "Acceptez les taux suggérés, ou renseignez vat_override_reason sur la ligne."
}

# Deux issues : (1) re-soumettre avec suggested_rate/suggested_category,
#               (2) assumer votre taux en ajoutant "vat_override_reason" sur la ligne.

In the SDKs, this 409 is typed: VatCorrectionRequiredException (PHP, getCorrections()/getHint()) and VatCorrectionRequiredError (JS, .corrections / .hint).

Electronic signatures (eIDAS)

Simple electronic signatures (EU-SES, eIDAS-compliant) on your PDFs, via OpenAPI.com. Multi-signer, SMS/email OTP, signature positions (in %, or pixels), UI customization, and download of the signed document + time-stamped proof. Built-in DOCX/DOC → PDF conversion (POST /signatures/convert-document).

bash
# Demande de signature électronique simple (EU-SES, eIDAS) sur un PDF
curl -X POST https://api.scell.io/api/v1/signatures \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "document_url": "https://votre-app.com/contrat.pdf",
    "document_name": "Contrat de prestation",
    "signers": [
      { "name": "Jean Dupont", "email": "jean@client.fr", "phone": "+33600000000",
        "message": "Merci de signer (code: {OTP})" }
    ],
    "signature_positions": [ { "page": 1, "x": 70, "y": 85, "unit": "percent" } ],
    "signature_options": { "signature_mode": "both", "signer_must_read": true, "timezone": "Europe/Paris" }
  }'

# Relancer / annuler / télécharger le document signé + la preuve
curl -X POST https://api.scell.io/api/v1/signatures/{id}/remind -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X POST https://api.scell.io/api/v1/signatures/{id}/cancel -H 'X-API-Key: sk_live_xxxxxxxx'
curl https://api.scell.io/api/v1/signatures/{id}/download/signed -H 'X-API-Key: sk_live_xxxxxxxx' -o signed.pdf
curl https://api.scell.io/api/v1/signatures/{id}/download/proof  -H 'X-API-Key: sk_live_xxxxxxxx' -o proof.pdf

Quotes, deposits & balances

Create signable quotes (public link, accept/refuse without an account), with a payment schedule, then convert them into deposit invoices (VAT due, Factur-X type 386) and balance invoices (type 380, automatic deduction of deposits). The quote audit trail is outside the ISCA chain (separate SHA-256 chain).

bash
# Créer un devis, l'envoyer, puis le convertir en acompte / solde
curl -X POST https://api.scell.io/api/v1/quotes \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "buyer_name": "Client SARL", "buyer_siret": "98765432109876",
    "lines": [ { "description": "Projet web", "quantity": 1, "unit_price": 10000, "tax_rate": 20 } ]
  }'
curl -X POST https://api.scell.io/api/v1/quotes/{id}/send -H 'X-API-Key: sk_live_xxxxxxxx'

# Conversion en facture d'acompte (TVA exigible) puis solde (déduit les acomptes)
curl -X POST https://api.scell.io/api/v1/quotes/{id}/convert-to-deposit \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' -d '{ "percent": 30 }'
curl -X POST https://api.scell.io/api/v1/quotes/{id}/convert-to-balance -H 'X-API-Key: sk_live_xxxxxxxx'

# Lien public signable (le client accepte/refuse sans compte) :
# GET /api/v1/public/quotes/{token}   POST …/accept   POST …/refuse   GET …/pdf

Payment schedule

Split a quote’s settlement into schedule lines (as a percentage of the total or a fixed amount, with a due date and milestone label). Each line can be converted into an invoice (deposit/balance), or auto-emitted at its due date via "auto_generate". POST replaces the whole schedule (max 50 lines), PATCH applies an add/update/remove batch.

bash
# Échéancier de paiement d'un devis : définir des lignes en % OU en montant.
# POST remplace l'INTÉGRALITÉ de l'échéancier (max 50 lignes).
curl -X POST https://api.scell.io/api/v1/quotes/{quoteId}/payment-schedule \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "lines": [
      { "amount_type": "percent", "amount_value": 30, "due_date": "2026-07-01",
        "milestone_label": "Acompte à la commande", "auto_generate": true },
      { "amount_type": "percent", "amount_value": 40, "due_date": "2026-08-15",
        "milestone_label": "Livraison V1" },
      { "amount_type": "amount", "amount_value": 3000.00, "due_date": "2026-09-30",
        "milestone_label": "Solde", "description": "Réception définitive" }
    ]
  }'
# amount_type : "percent" (% du total TTC du devis) | "amount" (montant fixe TTC)
# auto_generate : émet automatiquement la facture (acompte/solde) à l'échéance.

# Consulter, modifier par lot (add/update/remove), tout effacer
curl https://api.scell.io/api/v1/quotes/{quoteId}/payment-schedule          -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X PATCH  https://api.scell.io/api/v1/quotes/{quoteId}/payment-schedule -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' -d '{ "add": [ … ], "update": [ … ], "remove": [ "lineId" ] }'
curl -X DELETE https://api.scell.io/api/v1/quotes/{quoteId}/payment-schedule -H 'X-API-Key: sk_live_xxxxxxxx'

# Convertir une ligne d'échéance en facture (acompte/solde) + résumé agrégé
curl -X POST https://api.scell.io/api/v1/quotes/{quoteId}/payment-schedule/lines/{lineId}/convert -H 'X-API-Key: sk_live_xxxxxxxx'
curl https://api.scell.io/api/v1/quotes/{quoteId}/payment-summary -H 'X-API-Key: sk_live_xxxxxxxx'

Quote preview (non-persisted)

POST /api/v1/quotes/preview renders the PDF of an in-progress quote without persisting anything or consuming a number. For a live A4 HTML preview (branding + template), use POST /api/v1/documents/preview with type:"quote".

bash
# Aperçu PDF d'un devis EN COURS de saisie (rien n'est persisté, aucun numéro émis).
# Même corps qu'une création de devis ; renvoie le PDF directement.
curl -X POST https://api.scell.io/api/v1/quotes/preview \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "buyer_name": "Client SARL", "buyer_siret": "98765432109876",
    "currency": "EUR",
    "lines": [ { "description": "Projet web", "quantity": 1, "unit_price": 10000, "tax_rate": 20 } ],
    "notes": "Devis valable 30 jours."
  }' -o devis-preview.pdf
# Astuce : POST /api/v1/documents/preview (type:"quote") rend l'aperçu HTML A4 live
# (avec branding + modèle), utilisé par l'écran de création du dashboard.

Credit notes

A credit note ALWAYS targets an existing invoice. Total (full cancellation) or partial: a partial credit note selects lines from the source invoice (each line’s unit price and EXACT VAT rate are inherited — an invoice may mix 20% / 5.5% / exempt 0%). After issuing, the credit note is immutable (ISCA chain).

bash
# 1) Découvrir les lignes encore créditables d'une facture
curl https://api.scell.io/api/v1/invoices/{invoiceId}/remaining-creditable -H 'X-API-Key: sk_live_xxxxxxxx'
# -> { "data": { "items": [ { "invoice_line_id": "…", "remaining_quantity": 2, "tax_rate": 20 } ],
#                 "can_be_credited": true } }

# 2) Avoir PARTIEL : sélectionner des lignes de la facture (prix + taux hérités par ligne)
curl -X POST https://api.scell.io/api/v1/credit-notes \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{ "invoice_id": "019df46f-…", "reason": "Retour partiel", "type": "partial",
        "items": [ { "invoice_line_id": "…", "quantity": 1 } ] }'
# Avoir TOTAL : { "invoice_id": "…", "reason": "Annulation", "type": "total" }

# 3) Émettre (immuable après — chaîne ISCA)
curl -X POST https://api.scell.io/api/v1/credit-notes/{id}/send -H 'X-API-Key: sk_live_xxxxxxxx'

Buyers, suppliers & catalogue

Registries strictly scoped by (tenant, sub_tenant) — anti-IDOR. Idempotent on SIRET (B2B) or email (B2C). Once created, reuse a buyer via "buyer_id" on your invoices; its data is then frozen onto the issued invoice (fiscal immutability).

Suppliers are auto-derived from received invoices (POST /incoming-invoices) — no manual creation. Only email, phone, notes and metadata are editable via PATCH /api/v1/suppliers/:id. Identity fields (name, SIRET, address, etc.) come from the source invoice and are read-only.

The product/service catalogue (GET/POST /api/v1/products, GET/POST /api/v1/product-categories) lets you reuse line items: a quote, invoice or credit-note line can be pre-filled via "product_id", and the "save_to_catalog" flag stores a typed line into the catalogue (atomic server-side upsert, dedup by SKU then name). The "product_id" link on the line stays soft — the issued price/description remain frozen (ISCA immutability).

bash
# Acheteur réutilisable (idempotent sur SIRET en B2B / email en B2C)
curl -X POST https://api.scell.io/api/v1/buyers \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "name": "Client SARL", "is_individual": false, "country": "FR",
    "siret": "98765432109876", "vat_number": "FR12987654321", "email": "compta@client.fr",
    "billing_address": { "line1": "2 av. Lyon", "postal_code": "69001", "city": "Lyon", "country": "FR" }
  }'
# Puis réutiliser sur une facture : { "buyer_id": "<uuid>", "lines": [ … ] }
# Fournisseurs : dérivés automatiquement des factures reçues (POST /api/v1/incoming-invoices).
# GET /api/v1/suppliers  →  lister  |  GET /api/v1/suppliers/:id  →  détail
# PATCH /api/v1/suppliers/:id  →  enrichir uniquement email / phone / notes / metadata
# ⚠ Pas de création manuelle (POST) ni suppression (DELETE) — identité figée depuis la facture source.

Product / service catalogue

A product carries a default HT price, VAT rate and discount, a unit (UN/ECE code), a fiscal category (revenue_category: goods / service / accommodation) and, optionally, a custom organisation category. On an invoice, quote or credit-note line: "product_id" pre-fills the line; "save_to_catalog": true stores a typed line into the catalogue.

bash
# Catégorie d'organisation (custom, distincte de la catégorie fiscale)
curl -X POST https://api.scell.io/api/v1/product-categories \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{ "name": "Prestations web", "color": "#0066FF", "position": 1 }'

# Produit / service réutilisable
curl -X POST https://api.scell.io/api/v1/products \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "name": "Forfait audit SEO",
    "description": "Audit technique + plan d'\''action sur 3 mois",
    "sku": "SEO-AUDIT-3M",
    "revenue_category": "service",          // goods | service | accommodation (fiscal)
    "product_category_id": "<uuid catégorie>",
    "unit": "C62",                          // code UN/ECE (C62 = unité)
    "unit_price_ht": 1500.00,
    "default_tax_rate": 20,
    "default_discount_rate": 0,
    "currency": "EUR",
    "is_active": true
  }'

# Lister (recherche q + filtres), détail, mise à jour, suppression
curl 'https://api.scell.io/api/v1/products?q=audit&revenue_category=service&is_active=true' -H 'X-API-Key: sk_live_xxxxxxxx'
curl https://api.scell.io/api/v1/products/{id} -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X PATCH https://api.scell.io/api/v1/products/{id} -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' -d '{ "unit_price_ht": 1600 }'

# Réutiliser un produit sur une ligne de facture/devis/avoir : "product_id" pré-remplit
# description/prix/TVA/unité. "save_to_catalog": true enregistre une ligne saisie (upsert).
curl -X POST https://api.scell.io/api/v1/invoices \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "direction": "outgoing", "output_format": "facturx", "issue_date": "2026-06-05",
    "buyer_name": "Client SARL", "buyer_siret": "98765432109876",
    "lines": [
      { "product_id": "<uuid produit>", "quantity": 1 },
      { "description": "Maintenance mensuelle", "quantity": 1, "unit_price": 200, "tax_rate": 20,
        "save_to_catalog": true, "product_category_id": "<uuid catégorie>" }
    ]
  }'

Branding & live previews

Email branding (GET/PATCH /api/v1/branding/tenant, /branding/sub-tenants/:id variant) carries the logo, primary color, footer and signature of your transactional emails. The "brand_email_enabled" field toggles customization (false = default channel branding); "computed_email_footer" (read-only) exposes the footer computed from your company identity, used at render time when "brand_email_footer" is empty. The logo is uploaded either via presigned S3 (POST …/logo-upload-url) or directly as multipart (POST …/logo, "logo" field, jpeg/png/webp/svg, max 2 MB — SVGs are normalized automatically).

On the invoice side, templates (/api/v1/invoice-templates) gain the "is_enabled" field (false = the template is skipped by the resolution cascade, falling back to the system template, without losing its configuration) and the POST /invoice-templates/derive-colors-from-email-logo endpoint, which extracts the dominant colors of your email logo and applies them to the default template (404 if no logo, 422 if the hues are too neutral).

Finally, two live HTML previews: GET /branding/tenant/preview accepts query overrides (brand_primary_color, brand_email_footer, brand_email_signature, brand_logo_url) to try a setting before saving it, and POST /api/v1/documents/preview renders an in-progress document (invoice, credit note or quote) with the real template, branding and legal mentions of the issuing company — nothing is persisted (Cache-Control: no-store). This endpoint powers the real-time A4 preview on the dashboard creation screens.

bash
# Branding e-mail : lire / modifier (brand_email_enabled, couleurs, pied de page…)
curl https://api.scell.io/api/v1/branding/tenant -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X PATCH https://api.scell.io/api/v1/branding/tenant \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{ "brand_primary_color": "#0066FF", "brand_email_enabled": true }'

# Logo e-mail : upload direct multipart (jpeg/png/webp/svg, max 2 Mo)
curl -X POST https://api.scell.io/api/v1/branding/tenant/logo \
  -H 'X-API-Key: sk_live_xxxxxxxx' -F 'logo=@logo.svg'

# Aperçu HTML de l'e-mail brandé, avec overrides non persistés (essai avant enregistrement)
curl 'https://api.scell.io/api/v1/branding/tenant/preview?brand_primary_color=%230066FF' \
  -H 'X-API-Key: sk_live_xxxxxxxx'

# Appliquer au modèle de facture les couleurs extraites du logo e-mail
curl -X POST https://api.scell.io/api/v1/invoice-templates/derive-colors-from-email-logo \
  -H 'X-API-Key: sk_live_xxxxxxxx'

# Aperçu HTML live d'un document en cours de saisie (rien n'est persisté)
curl -X POST https://api.scell.io/api/v1/documents/preview \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' \
  -d '{
    "type": "invoice",
    "buyer": { "name": "Client SARL", "siret": "98765432109876" },
    "lines": [{ "description": "Prestation conseil", "quantity": 2, "unit_price": 450.00, "tax_rate": 20 }]
  }'

Credits & Scell.io billing

Scell.io is prepaid: you consume credits on each issuance. Track consumption and balance, top up (Stripe) or buy packs. Your consumption statements and Scell.io invoices are available via the API.

bash
# Consommation et solde du compte Scell.io (modèle prépayé)
curl https://api.scell.io/api/v1/tenant/billing/usage -H 'X-API-Key: sk_live_xxxxxxxx'
curl https://api.scell.io/api/v1/tenant/balance      -H 'X-API-Key: sk_live_xxxxxxxx'

# Recharger des crédits (Stripe PaymentIntent) ou acheter un pack
curl -X POST https://api.scell.io/api/v1/tenant/billing/top-up \
  -H 'X-API-Key: sk_live_xxxxxxxx' -H 'Content-Type: application/json' -d '{ "amount": 5000 }'
curl -X POST https://api.scell.io/api/v1/tenant/billing/packs/{packSlug}/checkout -H 'X-API-Key: sk_live_xxxxxxxx'

Error handling

HTTPMeaning
200 / 201Success
401Unauthenticated (TENANT_NOT_RESOLVED if key has no tenant)
402Insufficient balance (credits)
403KYB/KYC required, sub-tenant not ready (production)
404Not found / out of scope (SUB_TENANT_NOT_FOUND, anti-IDOR)
409Business conflict (VAT_CORRECTION_REQUIRED, QUOTE_NOT_EDITABLE)
422Validation (per-field) or fiscal rule (NO_ISSUER_COMPANY, SUB_TENANT_HAS_FISCAL_ENTRIES)
429Rate limit exceeded (Retry-After header)
5xxServer error
Error shape
javascript
// Corps d'erreur normalisé. La validation (422) détaille par champ ;
// les erreurs métier portent un code dans "error".
{ "message": "…", "errors": { "lines.0.tax_rate": ["…"] } }                  // 422 validation
{ "error": "VAT_CORRECTION_REQUIRED", "message": "…", "corrections": [ … ] } // 409 métier

// Codes métier utiles à intercepter :
//   401 TENANT_NOT_RESOLVED            clé sans tenant rattaché
//   404 SUB_TENANT_NOT_FOUND           sub_tenant_id hors scope (anti-IDOR)
//   422 NO_ISSUER_COMPANY              aucune company émettrice résolue
//   403 KYB_REQUIRED / KYC_REQUIRED    production : vérification incomplète
//   403 SUB_TENANT_NOT_READY           onboarding sous-tenant non vérifié (prod)
//   409 VAT_CORRECTION_REQUIRED        taux TVA incohérent (voir section TVA)
//   409 QUOTE_NOT_EDITABLE             devis verrouillé (accepté/signé)
//   422 SUB_TENANT_HAS_FISCAL_ENTRIES  suppression refusée (ledger ISCA)
//   429 rate limit dépassé (header Retry-After)

SDKs

Three official SDKs (v3.5.0) expose the whole API with strict typing, builders (invoice lines, quotes) and typed business exceptions (including VAT_CORRECTION_REQUIRED). The MCP server gives AI agents access to 148 tools (all prefixed scell_). Details and examples on the SDK page.

TypeScript / JS

@scell/sdk

v3.5.0

npm i @scell/sdk

PHP

scell/sdk

v3.5.0

composer require scell/sdk

MCP (agents IA)

@scell/mcp-client

v3.5.0 · 148 tools

npx @scell/mcp-client

Quick Start

Sandbox Mode Available: Use your own test publishable key pk_test_* (from your dashboard) to test the component in sandbox mode. Test data is not persisted.

The Scell.io onboarding component handles the entire company verification flow: SIREN/SIRET lookup, VAT number validation, address verification, and legal representative confirmation.

Step 1: Include the script

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

Step 2: Add the component

html
<scell-onboarding
  publishable-key="pk_live_xxxxxxxx"
  external-id="user_42"
  callback-url="https://votre-app.com/onboarding/done"
  theme="auto"
  locale="fr"
></scell-onboarding>

Step 3: Handle events

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

widget.addEventListener('onboarding:completed', (event) => {
  // event.detail respecte le type OnboardingCompletedPayload
  const { subTenant, credentials, externalId } = event.detail;

  // subTenant : { id, tenant_id, name, siret, vat_number, onboarding_status, is_active }
  //   onboarding_status : pending_superpdp | superpdp_redirected | superpdp_authorized
  //                       | superpdp_pending_review | active | superpdp_failed
  // credentials : { api_endpoint, parent_tenant_id, sub_tenant_id, external_id, superpdp_company_id }

  // A partir d'ici votre backend peut emettre des factures via :
  //   POST {credentials.api_endpoint}/invoices
  //   X-API-Key: <votre sk_live_*>            (cle parent — securisee, JAMAIS dans le navigateur)
  //   body: { "sub_tenant_id": subTenant.id, ... }   (scope sur le sub-tenant)
});

widget.addEventListener('onboarding:error', (event) => {
  console.error(event.detail.code, event.detail.message);
});

Live Demo

This demo uses a test publishable key. Create your account to get your own pk_test_* key and test the widget with your data.

Test the Scell.io Onboarding component directly below. This widget allows your users to verify their company (SIREN, VAT, legal representative) and complete their registration.

This widget runs in sandbox mode. To integrate it, see the Quick Start guide above.

API Reference

Attributes

Configure the component behavior using HTML attributes.

AttributeTypeRequiredDescription
publishable-keystringYesYour publishable key (pk_live_xxx or pk_test_xxx)
external-idstringNoYour internal identifier for this sub-tenant (link with your system)
callback-urlstringNoFull-page redirect URL after onboarding (used when widget is NOT inside a popup, e.g. embedded iframe without opener). Receives ?onboarding=success&sub_tenant_id=…&external_id=…
theme"light" | "dark" | "auto"NoColor theme. Default: "light"
locale"fr" | "en"NoWidget language. Default: "fr"
widthstringNoWidget width. Default: "100%"
heightstringNoWidget height. Default: "600px"
white-labelboolean (attribut HTML)NoWhite-label mode: hides the header with the Scell.io logo. The progress tracker and business content remain visible. Bare attribute (<scell-onboarding white-label>) is enough, or use "true" / "1" / "on" / "yes". Default: Scell.io header visible.

White-label mode

The white-label attribute hides the header with the Scell.io logo. The progress tracker and all business content (SIRET form, SUPER PDP redirect, completion screen) stay visible. Useful when you embed the widget without any visible Scell.io mention.

Accepted values: bare attribute (<scell-onboarding white-label>), or "true" / "1" / "on" / "yes". Any other value (including "false" and a missing attribute) keeps the header visible.

<!-- Mode marque blanche (attribut nu, ou white-label="true") -->
<scell-onboarding
  publishable-key="pk_live_xxxxxxxx"
  external-id="user_42"
  callback-url="https://votre-app.com/onboarding/done"
  theme="light"
  locale="fr"
  white-label
></scell-onboarding>

Events

Listen to component events to react to user actions and state changes.

EventPayloadDescription
onboarding:started{ sessionId: string }An onboarding session has been created on Scell.io. No SUPER PDP call yet.
onboarding:step{ step: "connect" | "redirect" | "complete", progress: number }The user moved to a new step (0% → 50% → 100%).
onboarding:completed{ subTenant: SubTenantSummary, credentials: OnboardingCredentials, externalId?: string }The Scell.io sub-tenant is created and ready for invoicing. SUPER PDP OAuth 2.1 tokens are encrypted at rest and used automatically by the backend to transmit invoices.
onboarding:error{ code: string, message: string }An error occurred (popup closed, invalid CSRF state, SUPER PDP KYB failure, etc.).

How it works

The <scell-onboarding> widget is the only official integration method. It drives an OAuth 2.1 + PKCE flow that creates both a Scell.io sub-tenant and the SUPER PDP registration in the same user session.

  1. 1The widget calls POST /onboarding/sessions with your publishable key (pk_*) to create a session linked to your parent tenant.
  2. 2The widget calls POST /onboarding/superpdp/authorize: the Scell.io backend generates PKCE (code_verifier/code_challenge S256), a CSRF state, and returns the SUPER PDP authorize URL.
  3. 3The widget opens a popup to that URL. The end user authenticates with SUPER PDP and completes KYB (SIREN, VAT, legal representative).
  4. 4SUPER PDP redirects to POST /onboarding/superpdp/callback with a code and the state.
  5. 5The Scell.io backend exchanges code+verifier for tokens (access + refresh), fetches the company info, and creates a sub-tenant linked to your parent tenant (with its own sk_/pk_ keys, invoice numbering, balance).
  6. 6The widget receives the scell:onboarding:complete event with the sub_tenant_id and initial credentials.

Why a single mode?

Since May 5th 2026, the 'Redirect Flow' and 'API Only' modes are removed. The widget covers every use case while guaranteeing OAuth 2.1 + PKCE compliance and parent/sub-tenant isolation. OAuth code exchange, refresh token rotation, and SUPER PDP KYB are handled by the Scell.io backend — you only embed one line of code.

Widget: SUPER PDP reconnect

Advanced widget variant. The mode="superpdp" mode exposes only the SUPER PDP step for a sub-tenant that is already created and validated. Use case: reconnect — or force the reconnection of — an existing sub-tenant, without going through identity, Sirene lookup or creation again. Requires the widget v3.2.0+ (CDN https://cdn.scell.io/widget/v1/onboarding.js).

2 steps: (1) server-side, your backend mints a signed token scoped to a sub-tenant using your sk_* key; (2) client-side, you embed the widget with that token. The resume token is an HMAC-signed URL bound to the targeted sub-tenant — a publishable pk_* key alone CANNOT target an arbitrary sub-tenant (anti-IDOR).

Step 1 — Mint the token (server-side)

Call POST /tenant/sub-tenants/{id}/superpdp-widget-token with your sk_* key (never exposed to the browser). The optional body { "reset": true } disconnects (revokes + resets) the sub-tenant before minting, to force a clean reconnection. The response returns a resume_token (signed URL, 24h TTL).

Mint the token — REST
bash
# 1) COTE SERVEUR — emettre un jeton signe scope a UN sub-tenant existant.
# La cle sk_* appartient au tenant parent : JAMAIS exposee au navigateur.
# body { "reset": true } deconnecte (revoque + reset) le sub-tenant AVANT
# d'emettre le jeton, pour FORCER une reconnexion SUPER PDP propre.
curl -X POST https://api.scell.io/api/v1/tenant/sub-tenants/{sub_tenant_id}/superpdp-widget-token \
-H 'X-API-Key: sk_live_xxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{ "reset": true }'
# Reponse 200 :
# {
# "resume_token": "https://api.scell.io/api/v1/widget/onboarding/sub-tenant/{id}/superpdp-resume?expires=1750000000&signature=abcd...",
# "expires_at": "2026-06-09T12:00:00Z" <- TTL 24h
# }
#
# Securite : 'resume_token' est une URL signee HMAC, scopee a CE sub-tenant
# (anti-IDOR). Une cle publishable pk_* seule ne permet PAS de cibler un
# sub-tenant arbitraire — seul le serveur (cle sk_*) peut emettre ce jeton.

Step 2 — Embed the widget (client-side)

Pass the resume_token obtained in step 1 into the resume-token attribute. The widget opens the SUPER PDP OAuth popup directly (single step) and emits the same events as the full flow (onboarding:completed, onboarding:error).

Embed in superpdp mode
html
<!-- 2) COTE CLIENT — embarquer le widget en mode "superpdp".
N'expose QUE l'etape SUPER PDP : ouvre directement la popup OAuth,
sans repasser par identite / Sirene / creation du sub-tenant.
Requiert le widget v3.2.0+. -->
<script src="https://cdn.scell.io/widget/v1/onboarding.js"></script>
<scell-onboarding
mode="superpdp"
resume-token="https://api.scell.io/api/v1/widget/onboarding/sub-tenant/{id}/superpdp-resume?expires=...&signature=..."
></scell-onboarding>
<script>
document.querySelector('scell-onboarding')
.addEventListener('onboarding:completed', (e) => {
// Memes events que le flux complet : { subTenant, credentials }
console.log('SUPER PDP reconnecte :', e.detail.subTenant.id);
});
document.querySelector('scell-onboarding')
.addEventListener('onboarding:error', (e) => {
console.error(e.detail.code, e.detail.message);
});
</script>
AttributeRequiredDescription
modeYesMust be "superpdp" to expose only the SUPER PDP step.
resume-tokenYesSigned URL minted in step 1 (POST .../superpdp-widget-token). Scoped to the targeted sub-tenant, 24h TTL.

Sandbox vs Production

Scell.io provides two fully isolated environments. The mode is automatically determined by the prefix of the publishable key you pass to the widget — no extra configuration needed.

CriterionSandboxProduction
Publishable key prefixpk_test_*pk_live_*
Secret key prefixsk_test_*sk_live_*
DatabaseIsolated. Test data, no fiscal value.Real data, retained 10 years (ISCA self-certification).
SUPER PDPSUPER PDP sandbox account. Invoices are NOT transmitted to the real Peppol/PPF network.SUPER PDP production account. Real transmission on the French electronic network (PPF).
SUPER PDP KYBSimplified KYB — any test SIREN works. No real verification.Full KYB (SIREN, VAT, legal representative verified by SUPER PDP).
Sub-tenantsCreated in the sandbox database, never visible from your live keys.Created in the production database, immediately operational for invoicing.
PricingFree, unlimited.See Pricing page.

How to switch

Just swap the value of the publishable-key attribute. The widget, the Scell.io backend, and the underlying SUPER PDP account all switch automatically. No code, API URL, or configuration changes required.

Example — switching from test to live

-<scell-onboarding publishable-key="pk_test_xxxxxxxx" />
+<scell-onboarding publishable-key="pk_live_xxxxxxxx" />

Best practices

  • Test in sandbox until the user journey is fully validated BEFORE switching to live.
  • Store live keys in server-side environment variables — NEVER in the browser.
  • pk_test_* keys are public by design (front-end OK). sk_test_*/sk_live_* keys are secret (back-end only).
  • An invoice issued in sandbox has no legal or fiscal value.

Framework Examples

Copy-paste examples for popular frameworks.

ScellOnboarding.tsx
tsx
import { useEffect, useRef } from 'react';
// Declarer le custom element pour TypeScript (React 19)
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'scell-onboarding': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'publishable-key': string;
'external-id'?: string;
'callback-url'?: string;
'theme'?: 'light' | 'dark' | 'auto';
'locale'?: 'fr' | 'en';
'width'?: string;
'height'?: string;
},
HTMLElement
>;
}
}
}
interface OnboardingResult {
subTenant: {
id: string;
tenant_id: string;
name: string;
siret: string | null;
vat_number: string | null;
onboarding_status:
| 'pending_superpdp'
| 'superpdp_redirected'
| 'superpdp_authorized'
| 'superpdp_pending_review'
| 'active'
| 'superpdp_failed';
is_active: boolean;
};
credentials: {
api_endpoint: string;
parent_tenant_id: string;
sub_tenant_id: string;
external_id: string | null;
superpdp_company_id: string | null;
};
externalId?: string;
}
interface Props {
publishableKey: string;
externalId?: string;
callbackUrl?: string;
onComplete?: (result: OnboardingResult) => void;
onError?: (err: { code: string; message: string }) => void;
}
export function ScellOnboarding({ publishableKey, externalId, callbackUrl, onComplete, onError }: Props) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
// Le script CDN s'auto-enregistre comme custom element global,
// donc on n'a a le charger qu'une seule fois par page.
if (document.querySelector('script[data-scell-widget]')) return;
const script = document.createElement('script');
script.src = 'https://cdn.scell.io/widget/v1/onboarding.js';
script.async = true;
script.dataset.scellWidget = 'true';
document.head.appendChild(script);
}, []);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handleComplete = (e: Event) => onComplete?.((e as CustomEvent<OnboardingResult>).detail);
const handleError = (e: Event) => onError?.((e as CustomEvent<{ code: string; message: string }>).detail);
el.addEventListener('onboarding:completed', handleComplete);
el.addEventListener('onboarding:error', handleError);
return () => {
el.removeEventListener('onboarding:completed', handleComplete);
el.removeEventListener('onboarding:error', handleError);
};
}, [onComplete, onError]);
return (
<scell-onboarding
ref={ref}
publishable-key={publishableKey}
external-id={externalId}
callback-url={callbackUrl}
theme="auto"
locale="fr"
/>
);
}

OAuth 2.1 redirect URIs registered with SUPER PDP

Reference info: Scell.io declared 3 official redirect_uris on the SUPER PDP side. You DO NOT need to configure anything to embed the widget — these URIs are used internally by Scell flows.

URLPurpose
/api/v1/widget/oauth-callbackCallback for the <scell-onboarding> widget embedded on your site. Creates the sub-tenant + returns sub_tenant + credentials to the widget via postMessage.
/api/v1/onboarding/superpdp/callbackLegacy POST endpoint kept for widget v1 integrations (back-compat). Same response contract as widget v2.
/api/v1/me/superpdp/callbackSelf-service onboarding from the Scell.io dashboard (logged-in tenant admin). Not related to widget integration.

Recurring Invoices

Automate invoice emission on a cadence (subscriptions, rent, maintenance contracts). You define a recurring profile once; Scell.io then emits each invoice on its due date, hands-free.

Key concepts

  • Editable template, frozen invoices. The recurring profile is an editable model. On each due date, Scell.io snapshots the CURRENT profile state into a real invoice, with its own ISCA chain. Editing the profile only affects FUTURE emissions — never invoices already issued.
  • auto_send submits the invoice to the PDP and emails the buyer with the Factur-X PDF. draft creates a draft for review (no sending).
  • Bilateral notifications + J-N reminder. Both issuer and recipient are notified on each cycle; a reminder is sent notify_before_days days before the due date.
  • Never silently skipped. A failed cycle produces a visible failed occurrence (with its error); it is never dropped without a trace.

Endpoints

Base: /api/v1. Auth: X-API-Key: sk_* (server-side only).

Method & pathDescription
GET /recurring-invoicesList (filters: status, sub_tenant_id, per_page)
POST /recurring-invoicesCreate a profile (201)
GET /recurring-invoices/{id}Get a profile
PUT /recurring-invoices/{id}Update (only affects future emissions)
DELETE /recurring-invoices/{id}Delete the profile (emitted invoices untouched)
GET /recurring-invoices/{id}/occurrencesOccurrence history
POST /recurring-invoices/{id}/pausePause
POST /recurring-invoices/{id}/activateActivate
POST /recurring-invoices/{id}/cancelCancel (terminal)
POST /recurring-invoices/{id}/run-nowEmit the next occurrence now (202)

Enums

FieldValues
recurrence.interval_unitday | week | month | year
end_modenever | on_date | after_occurrences
emission_modedraft | auto_send (default auto_send)
statusactive | paused | completed | cancelled
occurrence statuspending | emitted | failed | skipped

Create payload schema

title, lines, recurrence and start_date are required. The buyer is set via buyer_id (registry) OR the flat buyer_* fields. day_of_month is clamped to the month length (31 → 28/29/30).

POST /api/v1/recurring-invoices
json
{
"title": "Abonnement SaaS — Plan Pro", // requis
// Émetteur : sans sub_tenant_id => tenant master ; sinon le sous-tenant
"sub_tenant_id": "uuid", // optionnel
// Acheteur : buyer_id (registre) OU les champs buyer_* à plat
"buyer_id": "uuid", // optionnel (sinon buyer_*)
"buyer_name": "GN IMMO",
"buyer_country": "FR",
"buyer_is_individual": false, // true => B2C (siret/tva optionnels)
"buyer_siret": "49438068600076",
"buyer_vat_number": "FR42494380686",
"buyer_email": "compta@gnimmo.com",
"buyer_address": { "line1": "…", "postal_code": "…", "city": "…", "country": "FR" },
"buyer_shipping_address": { "name": "…", "line1": "…", "postal_code": "…", "city": "…", "country": "FR" },
"currency": "EUR", // optionnel, défaut "EUR"
"output_format": "facturx", // facturx | ubl | cii
"payment_terms": "Paiement à 30 jours.", // optionnel
// Lignes (requis) — même forme qu'une ligne de facture
"lines": [
{
"description": "Abonnement mensuel Plan Pro",
"quantity": 1,
"unit_price": 49.00,
"vat_rate": 20,
"unit": "mois", // optionnel
"discount": 0, // % remise sur la ligne, optionnel
"category": "subscription" // regroupement libre, optionnel
}
],
// Cadence (requis)
"recurrence": {
"interval_unit": "month", // day | week | month | year
"interval_count": 1, // tous les N (défaut 1)
"day_of_month": 1, // 1..31, clampé à la longueur du mois (31 => 28/29/30)
"day_of_week": 1 // 1..7 ISO (lun..dim) — uniquement si interval_unit "week"
},
"start_date": "2026-07-01", // requis (date ISO) — 1re émission
// Condition d'arrêt
"end_mode": "after_occurrences", // never | on_date | after_occurrences
"end_date": "2027-06-30", // requis si end_mode = "on_date"
"max_occurrences": 12, // requis si end_mode = "after_occurrences"
"emission_mode": "auto_send", // draft | auto_send (défaut auto_send)
"notify_before_days": 3, // 0..30 — rappel J-N avant chaque émission
"metadata": { "plan": "pro" } // optionnel
}

Examples

recurring-invoices.sh
bash
# Créer un profil de facture récurrente (la clé sk_* est server-side)
curl -X POST https://api.scell.io/api/v1/recurring-invoices \
-H 'X-API-Key: sk_live_xxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{
"title": "Abonnement SaaS — Plan Pro",
"buyer_id": "b1f2c3d4-5e6f-7081-92a3-b4c5d6e7f809",
"output_format": "facturx",
"lines": [
{ "description": "Abonnement mensuel Plan Pro", "quantity": 1, "unit_price": 49.00, "vat_rate": 20 }
],
"recurrence": { "interval_unit": "month", "interval_count": 1, "day_of_month": 1 },
"start_date": "2026-07-01",
"end_mode": "after_occurrences",
"max_occurrences": 12,
"emission_mode": "auto_send",
"notify_before_days": 3
}'
# Réponse 201 (extrait) :
# { "id": "r1e2c3u4-...", "status": "active", "next_occurrence_at": "2026-07-01" }
# Lister, mettre en pause, réactiver, émettre maintenant (202), annuler
curl https://api.scell.io/api/v1/recurring-invoices?status=active -H 'X-API-Key: sk_live_xxxxxxxx'
curl https://api.scell.io/api/v1/recurring-invoices/{id}/occurrences -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X POST https://api.scell.io/api/v1/recurring-invoices/{id}/pause -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X POST https://api.scell.io/api/v1/recurring-invoices/{id}/activate -H 'X-API-Key: sk_live_xxxxxxxx'
curl -X POST https://api.scell.io/api/v1/recurring-invoices/{id}/run-now -H 'X-API-Key: sk_live_xxxxxxxx' # 202 Accepted
curl -X POST https://api.scell.io/api/v1/recurring-invoices/{id}/cancel -H 'X-API-Key: sk_live_xxxxxxxx'

Webhooks

Receive real-time notifications when onboarding events occur. Configure your webhook URL in the dashboard.

Security: Always verify the webhook signature using the secret key from your dashboard. The signature is included in the x-scell-signature header.

Payload received in the widget (onboarding:completed event)

This payload is delivered both via window.opener.postMessage(...) (to the parent hosting the widget) and as event.detail on the onboarding:completed event of the <scell-onboarding> element.

scell:onboarding:complete
json
// Payload envoye par window.opener.postMessage(...) ET recu via
// l'event 'onboarding:completed' (event.detail) cote widget consumer.
{
"type": "scell:onboarding:complete",
"success": true,
"sub_tenant": {
"id": "01975f7c-...",
"tenant_id": "01975f78-...",
"name": "ACME SAS",
"siret": "12345678901234",
"vat_number": "FR12345678901",
"onboarding_status": "active",
"is_active": true
},
"credentials": {
"api_endpoint": "https://api.scell.io/api/v1",
"parent_tenant_id": "01975f78-...",
"sub_tenant_id": "01975f7c-...",
"external_id": "user_42",
"superpdp_company_id": "spp_company_99"
}
}

Issue an invoice right after onboarding

As soon as onboarding:completed fires, the sub-tenant is operational: SUPER PDP OAuth 2.1 tokens encrypted at rest, KYB validated, isolated invoice numbering. Zero wait.

POST /api/v1/invoices
javascript
// Apres l'event onboarding:completed, votre backend peut emettre des
// factures Factur-X au nom du sub-tenant immediatement. Les tokens
// OAuth 2.1 SUPER PDP sont chiffres en base et utilises automatiquement
// par Scell pour transmettre la facture chez SUPER PDP.
//
// IMPORTANT — Numerotation : ne jamais passer 'invoice_number' dans le
// body. La numerotation est entierement geree par Scell.io. La facture
// recoit un identifiant brouillon 'DRAFT-XXXXX' a la creation, puis un
// numero definitif 'XXXXX-YYYYMM-NNNNN' a l'emission (sequence chrono-
// logique sans rupture, conforme aux articles 242 nonies A et 289 du CGI).
// Le numero attribue est retourne dans la reponse (champ 'invoice_number').
// Modele d'auth : la cle sk_live_* appartient au TENANT parent. Pour
// emettre au nom d'un sub-tenant, on passe son 'sub_tenant_id' dans le
// body. La company emettrice est resolue automatiquement par Scell.io —
// aucun 'company_id' a fournir.
const res = await fetch('https://api.scell.io/api/v1/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SCELL_PARENT_SECRET_KEY, // sk_live_* (parent — JAMAIS dans le navigateur)
},
body: JSON.stringify({
sub_tenant_id: subTenant.id, // scope sur le sub-tenant
issue_date: '2026-05-05',
buyer_siret: '98765432100012',
buyer_name: 'Customer SAS',
lines: [
{ description: 'Prestation', quantity: 1, unit_price_excl_tax: 1000, vat_rate: 20 }
],
}),
});
// Reponse (extrait) :
// {
// "id": "inv_01975f80...",
// "invoice_number": "T0001-202605-00042", <- attribue par Scell.io
// "status": "issued",
// "issue_date": "2026-05-05",
// "total_excl_tax": 1000,
// ...
// }
const { invoice_number } = await res.json();

Available Webhook Events

EventDescription
sub_tenant.onboardedA Scell.io sub-tenant was just created via the widget. The payload contains sub_tenant_id, external_id, siret, onboarding_status. Persist the mapping in your database and enable invoicing for this client.
invoice.transmittedAn invoice issued for this sub-tenant was successfully transmitted to SUPER PDP. Includes superpdp_id and timestamp.
invoice.acceptedThe invoice was accepted by the recipient (Factur-X lifecycle).
invoice.rejectedThe invoice was rejected by the recipient or SUPER PDP. Payload includes rejection_reason.
invoice.incoming.receivedAn incoming invoice for this sub-tenant just arrived via SUPER PDP. Scell.io resolves it automatically by superpdp_company_id and exposes it in /tenant/incoming-invoices.
subtenant.threshold.warningA micro-entrepreneur sub-tenant reached 80% or 90% of a threshold (VAT franchise or micro ceiling). Payload: sub_tenant_id, category, kind, percent, projected_crossing_date.
subtenant.threshold.vat_base_exceededThe VAT franchise base threshold was exceeded: VAT becomes due on January 1st of the following year. It is up to the sub-tenant to act (VAT registration URSSAF/INPI).
subtenant.threshold.vat_majored_exceededThe VAT franchise increased threshold was exceeded: VAT is due immediately, retroactively to the 1st of the month of overshoot.
subtenant.threshold.micro_exceededThe micro-enterprise ceiling was exceeded: possible exit from the regime after 2 consecutive calendar years of overshoot.

Ready to integrate?

Create your account and get your API keys in minutes. Start with 100 free credits.

Your cookie preferences

We use cookies to improve your experience. Essential cookies are always active. Cookie policy.