# Scell.io REST API — SDK-agnostic reference (llms.txt) > Base URL: https://api.scell.io | API version: v1 (app 1.2.1) | Compliance: Autocertification ISCA (PAS NF525) | Updated: 2026-05-28 This document describes the public REST surface of Scell.io for integrators who want to consume the API directly over HTTP (no SDK), and for LLMs that need to answer integration questions accurately. Every endpoint, header, schema, status code, and field below maps 1:1 to the Laravel codebase (routes/api.php + Form Requests + Eloquent models + Resources). Nothing is invented. Three SDK-specific docs exist alongside this one: - `scell-sdk-js-llms.txt` (TypeScript / `@scell/sdk`) - `scell-sdk-php-llms.txt` (PHP / `scell/sdk` Composer) - `scell-mcp-agent-llms.txt` (Model Context Protocol bridge) If you are not writing raw HTTP, prefer one of those — they pre-bake retry logic, idempotency keys, error mapping, and TypeScript types. --- ## Table of contents 1. Overview & base URLs 2. Authentication (Sanctum cookie / sk_/pk_ keys / X-Tenant-Key legacy) 3. Sandbox vs Production switching 4. Schemas - 4.1 Address - 4.2 Buyer (registry) - 4.3 Company (issuer) - 4.4 Invoice (outgoing & incoming) - 4.5 CreditNote - 4.6 InvoiceTemplate - 4.7 SubTenant - 4.8 ApiKey - 4.9 Webhook - 4.10 Signature - 4.11 FiscalEntry / FiscalClosing / FiscalAnchor 5. Endpoints by resource - 5.1 Auth (`/v1/auth/*`) - 5.2 Buyers registry (`/v1/buyers`) - 5.3 Companies (`/v1/companies`) - 5.4 Invoices (`/v1/invoices`) - 5.5 Credit notes (`/v1/credit-notes`) - 5.6 Signatures (`/v1/signatures`) - 5.7 Webhooks (`/v1/webhooks`) - 5.8 API keys (`/v1/api-keys`) - 5.9 Billing & balance (`/v1/tenant/billing/*`, `/v1/tenant/balance`) - 5.10 Invoice templates (`/v1/tenant/invoice-templates`) - 5.11 Sub-tenants (`/v1/tenant/sub-tenants`) - 5.12 Fiscal compliance (`/v1/tenant/fiscal/*`) - 5.13 Onboarding & SuperPDP (`/v1/onboarding/*`) - 5.14 Tenant legacy surface (`/v1/tenant/*`) - 5.15 SIRENE lookup (`/v1/sirene/{siret}`) - 5.16 Pricing (`/v1/pricing/public`, `/v1/pricing`) - 5.17 Health check (`/api/health`) & version (`/v1/version`) - 5.18 Quotes (`/v1/quotes`) - 5.19 Payment schedule (`/v1/quotes/{id}/payment-schedule`) - 5.20 Credit packs (`/v1/packs/public`, `/v1/tenant/billing/packs/{slug}/checkout`) 6. End-to-end walkthroughs (curl) 7. Webhook delivery & HMAC verification 8. Errors (401/403/404/422/429/5xx) 9. Compliance (ISCA fiscal autocertification, Factur-X profiles, BR-* rules) 10. Versioning & change policy --- ## 1. Overview & base URLs Scell.io exposes a single REST API rooted at: ``` https://api.scell.io ``` All endpoints documented here live under `/api/v1/...`. The leading `/api` prefix is a Laravel convention; in practice you call: ``` GET https://api.scell.io/api/v1/invoices ``` There is **no separate sandbox host**. Sandbox traffic flows through the same hostname; routing to the sandbox database (`rdb_sandbox`) is decided by the prefix of the API key: | Key prefix | Database routed to | Mode | |-----------------|--------------------|------------| | `sk_test_…` | `rdb_sandbox` | Sandbox | | `pk_test_…` | `rdb_sandbox` | Sandbox | | `sk_live_…` | `rdb` (production) | Production | | `pk_live_…` | `rdb` (production) | Production | A middleware (`ResolveDatabaseConnection`) inspects the `X-API-Key` header or the `Authorization: Bearer …` token and switches the database connection on a per-request basis. Tenant rows, API keys and publishable keys live in the production DB only — even a `sk_test_*` request reads those from `rdb` while reading invoices/companies from `rdb_sandbox`. A second sandbox-only route prefix exists for a few endpoints: ``` POST https://api.scell.io/api/v1/sandbox/invoices GET https://api.scell.io/api/v1/sandbox/invoices/{id} POST https://api.scell.io/api/v1/sandbox/signatures … ``` These accept any `sk_*` key (test or live) but always force sandbox behaviour. They mirror a subset of the production routes for clients that want explicit, route-level isolation. Prefer `sk_test_*` against the regular `/api/v1/*` paths — the sandbox prefix exists for legacy callers. Content type is JSON unless noted otherwise: ``` Content-Type: application/json Accept: application/json ``` Responses are JSON. Numeric fields are sent as JSON numbers; monetary amounts as strings only inside Factur-X XML downloads (never inside JSON payloads). All timestamps are ISO 8601 with timezone (`2026-05-06T08:42:31+00:00`). --- ## 2. Authentication Scell.io accepts three authentication modes, evaluated in this order by the `FlexibleApiAuth` middleware on shared dashboard+SDK endpoints: ### 2.1 Sanctum SPA cookie (dashboard scell.io) Used by the React dashboard at `app.scell.io`. After `POST /api/v1/auth/login` the server sets an HttpOnly `XSRF-TOKEN` cookie + `scell_session` cookie. Subsequent requests automatically carry them — no header to add. Browser SPA only; not applicable to server-to-server calls. ```bash curl -X POST https://api.scell.io/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"founder@acme.fr","password":"…"}' \ -c cookies.txt curl https://api.scell.io/api/v1/auth/me \ -H 'X-XSRF-TOKEN: ' \ -b cookies.txt ``` ### 2.2 Secret API key `sk_*` (server-to-server) Header `X-API-Key` (preferred) **or** `Authorization: Bearer sk_…`. Required for any backend service that creates invoices, credit notes, signatures, or reads the buyers registry programmatically. Never expose in browser code. ```bash # Production curl https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_a2c4e6…' # Sandbox (same hostname, key prefix routes to rdb_sandbox) curl https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_test_b3d5f7…' # Authorization header alternative curl https://api.scell.io/api/v1/invoices \ -H 'Authorization: Bearer sk_live_a2c4e6…' ``` #### Auth model — the key belongs to a TENANT (since 2026-05-11) An `sk_*` key is bound to a **tenant**, never to a single company. The issuer of a document is resolved per request by `IssuerResolver`: - **Without `sub_tenant_id` in the POST body** → the action is performed on behalf of the **tenant master**; the emitting company is the tenant's `default_company`. - **With `sub_tenant_id` in the POST body** → the action is performed on behalf of that **sub-tenant**; the emitting company is the sub-tenant's first company. The sub-tenant must belong to the calling tenant, otherwise a `404` is returned (anti-IDOR). There is **no** `403 COMPANY_REQUIRED` anymore (removed). Production keys (`sk_live_*`) additionally enforce KYB (tenant master) / KYC (emitting company) / onboarding-ready (sub-tenant) before any issuance — see §8.2 for the full set of `IssuerResolver` error codes. Sandbox keys (`sk_test_*`) skip these checks. ### 2.3 Publishable key `pk_*` (client-side widgets) Header `X-Publishable-Key`. Used **only** by browser widgets to start an onboarding flow on a partner tenant — typically the `` custom element loaded from `https://cdn.scell.io/widget/v1/onboarding.js`. Cannot create invoices, sign documents, or read sensitive data. Limited to: - `POST /v1/onboarding/sessions` - `GET /v1/onboarding/sessions/{id}` - `POST /v1/onboarding/superpdp/authorize` - `POST /v1/onboarding/superpdp/callback` ```bash curl -X POST https://api.scell.io/api/v1/onboarding/sessions \ -H 'X-Publishable-Key: pk_live_5h7j9k…' \ -H 'Content-Type: application/json' \ -d '{"external_id":"merchant-42","redirect_url":"https://merchant.example/done"}' ``` #### `` HTML attributes | Attribute | Type | Description | |-----------|------|-------------| | `publishable-key` | `pk_live_*` / `pk_test_*` | Required. The partner publishable key. | | `theme` | `"light" \| "dark" \| "auto"` | Color theme. Default `"light"`. | | `locale` | `"fr" \| "en"` | UI language. Default `"fr"`. | | `external-id` | string | Optional. Your internal user/merchant ID, echoed back in callbacks. | | `callback-url` | URL | Optional. Full-page redirect after onboarding (when not in popup). | | `width` | CSS size | Widget width. Default `"100%"`. | | `height` | CSS size | Widget height. Default `"600px"`. | | `white-label` | boolean attribute | Hides the Scell.io header/logo. Progress tracker + business content remain. Bare `` is enough, or use `"true"` / `"1"` / `"on"` / `"yes"`. | ```html ``` ### 2.4 X-Tenant-Key (legacy multi-tenant routes `/v1/tenant/*`) The `/v1/tenant/*` family predates the unified `sk_*` flow. It accepts the same secret key but expects header `X-Tenant-Key` and (optionally) adds tenant-scoped helpers like sub-tenants and direct invoices. New integrations should use `/v1/invoices` with `X-API-Key` instead. The legacy surface is documented in §5.14 because existing partners still rely on it. ```bash curl https://api.scell.io/api/v1/tenant/me \ -H 'X-Tenant-Key: sk_live_a2c4e6…' ``` ### 2.5 Authentication matrix | Endpoint family | Sanctum cookie | sk_* (X-API-Key) | pk_* (X-Publishable-Key) | X-Tenant-Key | |--------------------------|:--------------:|:----------------:|:------------------------:|:------------:| | `/v1/auth/*` | ✓ | — | — | — | | `/v1/buyers` | ✓ | ✓ | — | — | | `/v1/invoices` | ✓ | ✓ | — | — | | `/v1/credit-notes` | ✓ | ✓ | — | — | | `/v1/signatures` (POST) | — | ✓ | — | — | | `/v1/signatures` (GET) | ✓ | ✓ | — | — | | `/v1/companies` | ✓ | — | — | — | | `/v1/api-keys` | ✓ | — | — | — | | `/v1/webhooks` | ✓ | — | — | — | | `/v1/onboarding/*` | — | — | ✓ | ✓ | | `/v1/tenant/*` | — | — | — | ✓ | | `/v1/sandbox/*` | — | ✓ (`sk_test_*`) | — | — | | `/api/health` | public | public | public | public | | `/v1/pricing/public` | public | public | public | public | A 401 is returned with body `{"message": "Unauthenticated."}` whenever the resolved auth context is empty. --- ## 3. Sandbox vs Production switching There is no environment flag in the request body. The system decides where to route based on the API key prefix: ``` sk_test_xxxxx → routed to rdb_sandbox (sandbox database) sk_live_xxxxx → routed to rdb (production database) pk_test_xxxxx → sandbox onboarding pk_live_xxxxx → live onboarding ``` Two kinds of data are routed differently: - **Tenant identity** (Tenant rows, API keys, publishable keys): always read from production DB (`rdb`). The methods `Tenant::findByApiKey()` and `Tenant::findByPublishableKey()` force connection `pgsql` even when the request is on sandbox. - **Business data** (invoices, credit notes, companies, buyers, signatures, fiscal entries, sub-tenants): scoped to the resolved DB. Sandbox data never leaks into production, and vice-versa. When developing: 1. Generate a `sk_test_*` key from the dashboard or via the sandbox provisioning command (`php artisan sandbox:sync --tenant=…`). 2. Hit production endpoints with that key. The middleware switches to `rdb_sandbox` for the duration of the request. 3. To run a smoke test that explicitly uses the sandbox routing prefix, call `/api/v1/sandbox/invoices` with the same `sk_test_*` key. Webhook deliveries from a sandbox tenant are tagged `environment: "sandbox"` in the payload (see §7). --- ## 4. Schemas UUIDs everywhere (HasUuids trait). All primary keys are strings of the form `0193af1a-7c64-7b9b-94e3-dbcfe4ff0000` (UUID v7). Timestamps are ISO 8601. Money fields are JSON numbers, two decimals. ### 4.1 Address Used inside `Buyer.billing_address`, `Buyer.shipping_address`, `Invoice.seller_address`, `Invoice.buyer_address`, `Invoice.buyer_shipping_address`, and `Company.address_*`. | Field | Type | Required | Description | |---------------|---------|:--------:|-------------------------------------------------------| | `name` | string | no | Recipient name (BT-74 SHIP TO NAME). `shipping_address` only. | | `line1` | string | yes | Street + number (BT-50 / BT-75) | | `line2` | string | no | Apartment / floor / additional line (BT-51 / BT-76) | | `postal_code` | string | yes | ZIP / CP (BT-53 / BT-78) | | `city` | string | yes | City (BT-52 / BT-77) | | `region` | string | no | State / region / department (BT-54 / BT-79) | | `country` | string | yes | ISO 3166-1 alpha-2, uppercase (BT-55 / BT-80) | The `region` field is propagated to Factur-X CountrySubDivisionName (BT-79) when present. Omit for purely postal addresses. The legacy `street`/`street2` keys (used historically by the legacy `/v1/tenant/*` invoice surface) map to `line1`/`line2`. New integrations should send `line1`/`line2`. ### 4.2 Buyer (registry) Persisted in `buyers` table (UUID PK + soft delete). Scoped strictly by `(tenant_id, sub_tenant_id)`. A buyer is **the current state** of a customer's identity & addresses. Invoices keep their own snapshot of the buyer at issuance time (denormalised `buyer_*` columns), so updating a buyer never mutates historical invoices (ISCA immutability). | Field | Type | Required (POST) | Description | |--------------------|----------|:---------------:|------------------------------------------------------------| | `id` | uuid | server | Stable identifier — pass it as `buyer_id` on invoices. | | `tenant_id` | uuid | server | Auto from auth context. | | `sub_tenant_id` | uuid? | no | Pass in the POST body to scope the buyer to a sub-tenant; omit for the tenant master. Must belong to the calling tenant (404 otherwise). | | `name` | string | yes | Legal name or natural person name (max 255). | | `is_individual` | boolean | no | `true` ⇒ B2C particulier. SIRET/VAT/legal_id optional. | | `siret` | string? | conditional | 14 digits, mandatory for FR B2B (or pass VAT/legal_id). | | `vat_number` | string? | conditional | EU VAT (max 20). Mandatory for non-FR B2B if no legal_id. | | `legal_id` | string? | conditional | Foreign legal id (max 50). | | `legal_id_scheme` | string? | no | Scheme code (max 10). | | `email` | string? | no | Contact email (max 255). | | `phone` | string? | no | E.164 / national format (max 50). | | `country` | string | yes | ISO 3166-1 alpha-2 of the legal entity. | | `billing_address` | Address | yes | Used as Factur-X BG-12 BUYER POSTAL ADDRESS. | | `shipping_address` | Address? | no | Used as BG-13 SHIP TO when distinct from `billing_address`.| | `metadata` | object? | no | Free-form (JSON object). Not propagated to Factur-X. | | `notes` | string? | no | Internal notes (max 5000). | | `created_at` | datetime | server | | | `updated_at` | datetime | server | | | `deleted_at` | datetime?| server | Soft delete. | Identity rules (enforced by `StoreBuyerRequest`): - B2C (`is_individual: true`): no fiscal id required at all. - B2B FR (`country: "FR"`, `is_individual: false`): one of SIRET / VAT / legal_id required. - B2B non-FR: one of VAT / legal_id required. If `shipping_address` is **byte-equal** to `billing_address` (via `Buyer::addressesEqual`), the controller silently stores `null` for it to keep the Factur-X BG-13 emission rule consistent (ISCA Schematron warns when SHIP TO duplicates BUYER POSTAL ADDRESS). ### 4.3 Company (issuer) Persisted in `companies` table. Represents an entity that issues invoices on Scell.io. A tenant can own several companies; sub-tenant scoping is optional. | Field | Type | Required (POST) | Description | |-----------------------------|----------|:---------------:|---------------------------------------------------| | `id` | uuid | server | | | `user_id` | uuid | server | Owner. | | `tenant_id` | uuid | server | | | `sub_tenant_id` | uuid? | optional | Set when company belongs to a sub-tenant scope. | | `name` | string | yes | Legal name. | | `siret` | string | yes (FR) | 14 digits. | | `vat_number` | string? | conditional | EU VAT. | | `legal_id` | string? | conditional | Foreign legal id. | | `legal_id_scheme` | string? | no | | | `legal_form` | string? | no | SAS, SARL, EURL, EI, … | | `address_line1` | string | yes | | | `address_line2` | string? | no | | | `postal_code` | string | yes | | | `city` | string | yes | | | `country` | string | yes | ISO alpha-2. | | `phone` | string? | no | | | `email` | string? | no | | | `website` | string? | no | | | `logo_url` | string? | no | Legacy logo (deprecation programmed, see CLAUDE.md).| | `iban` | string? | no | Default IBAN (BT-84 PAYEE FINANCIAL ACCOUNT). | | `bic` | string? | no | Default BIC (BT-86 PAYEE SERVICE PROVIDER). | | `payment_terms_default` | string? | no | BT-20 default. Overridable per invoice. | | `payment_due_days_default` | integer? | no | Helper to compute `due_date = invoice_date + N`. | | `invoice_footer_default` | string? | no | PDF footer text (mentions légales, RCS, capital). | | `invoice_notes_default` | string? | no | BT-22 default note. Overridable per invoice. | | `status` | enum | server | `pending`, `active`, `suspended`. | | `kyc_completed_at` | datetime?| server | | | `kyc_reference` | string? | server | | | `superpdp_id` | string? | server | SUPER PDP company id once provisioned. | | `metadata` | object? | no | | `iban` / `bic` / `payment_terms_default` / `payment_due_days_default` / `invoice_footer_default` / `invoice_notes_default` are SDK-overridable on each invoice. Hierarchy: invoice payload > Company default. ### 4.4 Invoice (outgoing & incoming) Persisted in `invoices` table. Sources of truth for fiscal compliance: the denormalised buyer/seller fields are immutable once the invoice leaves draft (ISCA guard, see §9). | Field | Type | Required (POST) | Description | |-----------------------------|----------|:---------------:|--------------------------------------------------------| | `id` | uuid | server | | | `user_id` | uuid | server | | | `tenant_id` | uuid | server | | | `company_id` | uuid | conditional | Tenant-direct flow: required (`seller_company_id`). | | `api_key_id` | uuid? | server | API key used at creation. | | `environment` | enum | server | `production` or `sandbox`. | | `external_id` | string? | no | Caller's idempotency / correlation id (max 100). | | `invoice_number` | string | server | Generated exclusively by Scell.io. Never accept input. | | `direction` | enum | yes | `outgoing` (you bill someone) or `incoming` (supplier).| | `input_format` | enum? | server | Original format if uploaded for conversion. | | `output_format` | enum | yes | `facturx`, `ubl`, `cii`. Default `facturx`. | | `issue_date` | date | yes | YYYY-MM-DD. Cannot be in the future. | | `due_date` | date | yes | ≥ `issue_date`. | | `currency` | string | yes | ISO 4217 (`EUR`). | | `total_ht` | number | yes | Sum of line `total_ht` (BT-106). | | `total_tax` | number | yes | (BT-110) | | `total_ttc` | number | yes | (BT-112) | | `seller_company_id` | uuid | yes (legacy) | `/v1/tenant/invoices`: company that issues. | | `seller_name` | string | yes (flex) | `/v1/invoices`: flat seller fields. | | `seller_country` | string | yes | | | `seller_siret` | string? | conditional | FR ⇒ required. | | `seller_vat_number` | string? | conditional | | | `seller_legal_id` | string? | conditional | | | `seller_legal_id_scheme` | string? | no | | | `seller_address` | Address | yes | | | `buyer_id` | uuid? | optional | Registry shortcut. When set, flat `buyer_*` optional. | | `buyer_name` | string | yes (no buyer_id)| | | `buyer_country` | string | yes (no buyer_id)| ISO alpha-2. | | `buyer_is_individual` | boolean | no | `true` ⇒ B2C. SIRET/VAT/legal_id optional, BT-46/47/48 omitted (BR-CO-26).| | `buyer_siret` | string? | conditional | FR B2B ⇒ required. | | `buyer_vat_number` | string? | conditional | | | `buyer_legal_id` | string? | conditional | | | `buyer_legal_id_scheme` | string? | no | | | `buyer_email` | string? | no | Tenant-direct only. | | `buyer_phone` | string? | no | Tenant-direct only. | | `buyer_address` | Address | yes (no buyer_id)| BG-12 BUYER POSTAL ADDRESS. | | `buyer_shipping_address` | Address? | no | BG-13 SHIP TO. Omit ⇒ EN16931 presumption "ship to = bill to".| | `lines` | InvoiceLine[]| yes | `min:1`. See below. | | `payment_terms` | string? | no | BT-20. | | `payment_method` | enum? | no | `virement`, `carte`, `cheque`, `especes`, `prelevement` (legacy `/v1/tenant/*` only).| | `bank_details.iban` | string? | no | Override company default. ISO 13616. | | `bank_details.bic` | string? | no | Override company default. | | `bank_details.bank_name` | string? | no | | | `notes` | string? | no | BT-22. Max 2000. | | `internal_reference` | string? | no | Tenant-direct only. Max 100. | | `purchase_order` | string? | no | BT-13 (Buyer reference / order ref). Max 50. | | `contract_reference` | string? | no | BT-12. Max 50. | | `metadata` | object? | no | Free-form. Not in Factur-X. | | `original_file` | file? | no | Multipart upload. PDF/XML conversion path. | | `archive_enabled` | boolean | no | Default true. | | `archive_status` | enum | server | | | `archive_path` | string? | server | | | `archive_until` | date? | server | Retention date. | | `send_immediately` | boolean | no | Tenant-direct only. Submits right after creation. | | `send_via` | enum? | conditional | `email`, `peppol`, `chorus`. Required if `send_immediately:true`.| | `invoice_template_id` | uuid? | no | Personalisation template. Cascade resolution: explicit > sub_tenant default > tenant default > system default.| | `status` | enum | server | See "Status enum" below. | | `legal_status` | enum | server | Reflects fiscal lifecycle (validated, transmitted, paid…).| | `validated_at` | datetime?| server | | | `transmitted_at` | datetime?| server | | | `received_at` | datetime?| server | Incoming only. | | `accepted_at` | datetime?| server | Incoming only. | | `rejected_at` | datetime?| server | | | `rejection_reason` | string? | server | | | `paid_at` | datetime?| server | | | `payment_reference` | string? | server | | | `read_at` | datetime?| server | Incoming only. `markAsRead` is idempotent. | | `read_by_user_id` | uuid? | server | | | `format_paths` | object? | server | Map of generated file URIs by format. | #### Invoice line (`lines.*`) | Field | Type | Required | Description | |-----------------|---------|:--------:|-------------------------------------------------------| | `description` | string | yes | Item label (BT-153). Max 500. | | `quantity` | number | yes | > 0. (BT-129) | | `unit` | string? | no | UN/ECE Rec 20 code. (BT-130) Max 20. | | `unit_price` | number | yes | (`/v1/tenant/*`) Unit price HT. (BT-146) | | `unit_price_ht` | number | yes | (`/v1/invoices`) Unit price HT. (BT-146) | | `vat_rate` | number | yes | (`/v1/tenant/*`) Allowed values: 0, 2.1, 5.5, 10, 20.| | `tva_rate` | number | yes | (`/v1/invoices`) Same allowed values. | | `total_ht` | number | yes | (BT-131) `quantity × unit_price × (1 - discount/100)`.| | `total_ttc` | number | yes | | | `discount` | number? | no | 0..100 percent. | | `product_code` | string? | no | (BT-155) Internal SKU. | | `metadata` | object? | no | | #### Invoice status enum `status` field — internal lifecycle. | Value | Meaning | |---------------|------------------------------------------------------------------------| | `draft` | Created, not yet validated. Editable. Fiscal fields still mutable. | | `validated` | Passed Factur-X / UBL / CII validation. Immutable from now. | | `converted` | Converted from PDF to Factur-X (legacy flow). | | `transmitted` | Sent to PDP / Chorus / Peppol. Awaiting acknowledgment. | | `accepted` | Acknowledged by counterpart / authority. (Incoming or outgoing.) | | `rejected` | Rejected by validator or counterpart. | | `paid` | Payment recorded. Outgoing & incoming. | | `completed` | End state for outgoing invoices. | `legal_status` mirrors a stricter fiscal lifecycle used by the ISCA ledger (immutable history). Treat both as read-only. ### 4.5 CreditNote Persisted in `credit_notes` table. Strictly linked to a parent `Invoice` (field `invoice_id`). Inherits buyer/seller from the parent and forbids override of those fields (validated by `InvoiceCreditable` rule). | Field | Type | Required (POST) | Description | |------------------------|-----------|:---------------:|---------------------------------------------------| | `id` | uuid | server | | | `company_id` | uuid | server | | | `invoice_id` | uuid | yes | Must be creditable (validated/transmitted/accepted/paid/completed) and not fully credited.| | `user_id` | uuid | server | | | `api_key_id` | uuid? | server | | | `credit_note_number` | string | server | Generated by Scell.io. | | `reference` | string? | no | External reference (max 100). | | `status` | enum | server | `draft`, `sent`. | | `legal_status` | enum | server | | | `is_locked` | boolean | server | True once sent. | | `locked_at` | datetime? | server | | | `sent_at` | datetime? | server | | | `type` | enum | yes | `total` (full refund), `partial`. | | `reason` | string? | no | Free text. | | `subtotal` | number | yes (server-derived from items) | | | `tax_amount` | number | yes | | | `total` | number | yes | | | `currency` | string | yes | | | `issue_date` | date | yes | | | `facturx_profile` | enum? | server | `MINIMUM`, `BASIC_WL`, `BASIC`, `EN_16931` (default), `EXTENDED`.| | `facturx_path` | string? | server | | | `metadata` | object? | no | | | `is_sandbox` | boolean | server | | | `tenant_id` | uuid | server | | | `sub_tenant_id` | uuid? | server | | | `buyer_id` | uuid? | server | Inherited from parent invoice if any. | | `buyer_name` | string | server | Snapshot inherited from invoice. | | `buyer_email` | string? | server | | | `buyer_siret` | string? | server | | | `buyer_siren` | string? | server | | | `buyer_vat_number` | string? | server | | | `buyer_address` | Address | server | | | `buyer_shipping_address`| Address? | server | Inherited from invoice if any. | | `seller_name` | string | server | | | `seller_siret` | string? | server | | | `seller_address` | Address | server | | | `buyer_is_individual` | boolean | server | Propagated from parent invoice. | | `invoice_template_id` | uuid? | no | Override invoice's template. | | `items` | CreditNoteItem[] | yes | | CreditNote is **non-deletable** once sent (ISCA `static::deleting` guard throws `RuntimeException`). Use cancellation flows from the dashboard instead. Drafts can be deleted. ### 4.6 InvoiceTemplate Persisted in `invoice_templates`. Personalises invoice/credit-note PDF layout (logo, footer text, palette, custom CSS). | Field | Type | Required (POST) | Description | |-----------------|----------|:---------------:|------------------------------------------| | `id` | uuid | server | | | `tenant_id` | uuid | server | | | `sub_tenant_id` | uuid? | server | | | `scope` | enum | server | `system`, `tenant`, `sub_tenant`. | | `name` | string | yes | | | `is_default` | boolean | no | Only one per scope. | | `logo_url` | string? | no | Override `Company.logo_url`. | | `palette` | object? | no | `{primary: "#…", secondary: "#…"}`. | | `footer_html` | string? | no | | | `custom_css` | string? | no | | | `metadata` | object? | no | | Resolution cascade: explicit `invoice_template_id` on invoice/credit-note > sub_tenant default > tenant default > system default. Cached in Redis for 5 minutes. ### 4.7 SubTenant Children entities of a tenant — used by partner/marketplace tenants to let their own customers issue invoices through Scell.io. Persisted in `sub_tenants`. | Field | Type | Required (POST) | Description | |-------------------|----------|:---------------:|-----------------------------------------------------| | `id` | uuid | server | | | `tenant_id` | uuid | server | | | `external_id` | string? | no | Partner's internal id (lookup via `by-external-id`).| | `name` | string | yes | Legal name. | | `email` | string? | no | | | `contact_email` | string? | no | Used for daily fiscal closure emails. | | `siret` | string? | conditional | | | `vat_number` | string? | conditional | | | `country` | string | yes | | | `address` | Address | yes | | | `superpdp_id` | string? | server | Set after SuperPDP onboarding. | | `kyb_status` | enum | server | `pending`, `verified`, `rejected`. | | `metadata` | object? | no | | | `created_at` | datetime | server | | | `updated_at` | datetime | server | | ### 4.8 ApiKey | Field | Type | Description | |-----------------|----------|---------------------------------------------------------------| | `id` | uuid | | | `name` | string | Human label. | | `prefix` | string | `sk_test`, `sk_live`, `pk_test`, `pk_live`. | | `last4` | string | Last 4 chars (full key shown only at creation). | | `tenant_id` | uuid | | | `company_id` | uuid? | Scope to a specific company (sub-tenant invoicing). | | `scopes` | string[] | Reserved for future granular scopes. | | `last_used_at` | datetime?| | | `expires_at` | datetime?| Null for non-expiring keys. | | `created_at` | datetime | | | `revoked_at` | datetime?| | The full secret is returned **only** on `POST /v1/api-keys`. Subsequent `GET` calls return only `prefix` + `last4`. ### 4.9 Webhook | Field | Type | Description | |---------------|----------|------------------------------------------------------------------| | `id` | uuid | | | `tenant_id` | uuid | | | `url` | string | HTTPS only. | | `events` | string[] | Subscribed events. See §7 for list. | | `secret` | string | HMAC-SHA256 secret. Returned on creation/regeneration only. | | `is_active` | boolean | | | `description` | string? | | | `created_at` | datetime | | | `updated_at` | datetime | | ### 4.10 Signature eIDAS EU-SES (Simple Electronic Signature). Backed by an upstream provider **Scell.io only sells EU-SES** — AES and QES are not exposed. #### Signing URL wrapper (sign.scell.io) Since 2026-05-10, `signers[].signing_url` returned in the response no longer points to the upstream provider directly. Instead, every signer receives a Scell.io-hosted wrapper URL of the form: ``` https://sign.scell.io/sign/{signature_id}/{signer_id}?expires=…&signature=HMAC ``` The wrapper page embeds the upstream signing flow inside an iframe and applies Scell.io branding by default. URLs are HMAC-signed with Laravel `temporarySignedRoute` (TTL = signature `expires_at` + 1h, default +30 days). Tampering returns HTTP 403. The original upstream URL is also stored on `signers[].upstream_signing_url` for debugging / migration purposes; integrators should always distribute the `signing_url` (Scell wrapper) to end-users. #### Default branding behaviour When the request omits `ui_config`, the backend fills the 16 visual fields with the Scell.io palette (logo, colors). When `ui_config` is partial, only the missing fields receive defaults — explicit values are preserved as-is. `iframe_ancestors` is always automatically extended with `https://sign.scell.io` and `https://scell.io` (deduplicated, capped at 20 entries) so the wrapper page can embed the upstream iframe regardless of CSP. | Field | Type | Required (POST) | Description | |-------------------------|-----------|:---------------:|-------------------------------------------------------------------| | `id` | uuid | server | | | `tenant_id` | uuid | server | | | `company_id` | uuid | server | | | `external_id` | string? | no | | | `document` | object | yes | `{file_url|file_base64, file_name, file_hash?}` | | `signers` | Signer[] | yes | At least one. Order matters. | | `signature_positions` | Pos[] | yes | Where to drop the signature box. | | `signature_options` | object? | no | `{signature_mode, signer_must_read, user_editable_data, timezone}`| | `ui_config` | object? | no | 21 fields: sidebar/header/footer/buttons + `iframe_ancestors`. | | `expires_at` | datetime? | no | | | `redirect_url` | string? | no | Where the signer is redirected post-signing. | | `webhook_url` | string? | no | Override default per-call. | | `status` | enum | server | `pending`, `waiting_signers`, `partially_signed`, `completed`, `refused`, `expired`, `error`. | | `signed_pdf_url` | string? | server | Available once `completed`. | | `audit_trail_url` | string? | server | PDF audit trail (eIDAS). | Signer object: | Field | Type | Description | |-----------------|----------|------------------------------------------------------| | `email` | string | Required. | | `first_name` | string | | | `last_name` | string | | | `phone` | string? | E.164. Used for SMS OTP. | | `language` | enum? | `fr`, `en`. Default `fr`. | | `auth_mode` | enum? | `email`, `sms`. Default `email`. | | `message` | string? | Custom message (max 500). `{OTP}` placeholder works. | | `order` | integer? | Sequential order; lower = earlier. | Position object: | Field | Type | Description | |----------------|---------|----------------------------------------------------------| | `signer_email` | string | Maps the box to a specific signer. | | `page` | integer | 1-based. | | `x`, `y` | number | Top-left corner. | | `width`, `height` | number | | | `unit` | enum | `percent` (default) or `pixel`. | | `page_width_px`, `page_height_px` | number? | Required if `unit:"pixel"` and PDF parse fails.| ### 4.11 Fiscal entities (read-only) | Entity | Table | Purpose | |---------------|-------------------|-------------------------------------------------------------------| | FiscalEntry | `fiscal_entries` | Immutable hash-chained log entry per invoice/credit note. | | FiscalClosing | `fiscal_closings` | Daily / monthly / annual closure. Includes `closing_hash`. | | FiscalAnchor | `fiscal_anchors` | RFC 3161 TSA + OpenTimestamps Bitcoin proofs. | | FiscalRule | `fiscal_rules` | LF 2026 normative rules. | | FiscalSequence| `fiscal_sequences`| Per (tenant_id, sub_tenant_id) sequence counter. | All exposed via `GET /v1/tenant/fiscal/*` and never via write APIs from the SDK surface (only through the daily scheduler). --- ## 5. Endpoints by resource ### 5.1 Auth — `/v1/auth/*` #### `POST /api/v1/auth/register` Public. Creates a Scell.io account and the underlying tenant. ```bash curl -X POST https://api.scell.io/api/v1/auth/register \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme founder", "email": "founder@acme.fr", "password": "5tr0ng-pa55w0rd", "password_confirmation": "5tr0ng-pa55w0rd", "tenant_name": "Acme SAS" }' ``` Returns `201` with the user, the tenant, and an initial Sanctum token. #### `POST /api/v1/auth/login` Public. Authenticates email/password. Sets Sanctum SPA cookies if called from a browser, returns a Bearer token otherwise. ```bash curl -X POST https://api.scell.io/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"founder@acme.fr","password":"…"}' ``` Response: ```json { "user": { "id": "01...", "email": "founder@acme.fr", "name": "Acme founder" }, "tenant": { "id": "01...", "name": "Acme SAS" }, "token": "1|abc..." } ``` #### `POST /api/v1/auth/forgot-password` Public. Triggers a password-reset email. ```bash curl -X POST https://api.scell.io/api/v1/auth/forgot-password \ -H 'Content-Type: application/json' \ -d '{"email":"founder@acme.fr"}' ``` #### `POST /api/v1/auth/reset-password` Public. Consumes the reset token. ```bash curl -X POST https://api.scell.io/api/v1/auth/reset-password \ -H 'Content-Type: application/json' \ -d '{ "token": "abc123…", "email": "founder@acme.fr", "password": "n3w-pa55w0rd", "password_confirmation": "n3w-pa55w0rd" }' ``` #### `POST /api/v1/auth/logout` Sanctum-protected. Revokes the current token. ```bash curl -X POST https://api.scell.io/api/v1/auth/logout \ -H 'Authorization: Bearer 1|abc...' ``` #### `GET /api/v1/auth/me` Sanctum-protected. Returns the current user, tenant, and roles. ```bash curl https://api.scell.io/api/v1/auth/me \ -H 'Authorization: Bearer 1|abc...' ``` #### `PUT /api/v1/auth/tenant/profile` Updates tenant name / email / SIRET / address. Sanctum-only. #### `POST /api/v1/auth/tenant/keys/publishable` Generates a `pk_*` publishable key. #### `POST /api/v1/auth/tenant/logo-upload-url` Returns a pre-signed S3 URL for the tenant logo. #### `GET /api/v1/auth/google`, `GET /api/v1/auth/google/callback` Google OAuth login flow. Browser redirects only. ### 5.2 Buyers registry — `/v1/buyers` Shared dashboard + SDK surface (FlexibleApiAuth + tenant.isolation). Strict scoping by `(tenant, sub_tenant)`. Pagination 25 default, 100 max. #### `GET /api/v1/buyers` Query parameters: | Param | Type | Description | |----------------|---------|---------------------------------------------------| | `q` | string | Partial match on name / siret / email. | | `is_individual`| boolean | Filter B2C vs B2B. | | `per_page` | integer | 1..100. Default 25. | | `page` | integer | 1-based. | ```bash curl 'https://api.scell.io/api/v1/buyers?q=acme&per_page=50' \ -H 'X-API-Key: sk_live_…' ``` Response: ```json { "data": [ { "id": "0193af1a-7c64-7b9b-94e3-dbcfe4ff0001", "name": "Acme SAS", "is_individual": false, "siret": "12345678901234", "vat_number": "FR12345678901", "email": "billing@acme.fr", "country": "FR", "billing_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "shipping_address": null, "created_at": "2026-04-12T08:00:00+00:00", "updated_at": "2026-05-04T10:24:11+00:00" } ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "from": 1, "to": 25, "total": 173, "per_page": 25 } } ``` #### `POST /api/v1/buyers` ```bash curl -X POST https://api.scell.io/api/v1/buyers \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme SAS", "country": "FR", "siret": "12345678901234", "vat_number": "FR12345678901", "email": "billing@acme.fr", "billing_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "shipping_address": { "name": "Entrepot Lyon", "line1": "12 Avenue Saxe", "postal_code": "69003", "city": "Lyon", "country": "FR" } }' ``` Returns `201` with the created Buyer. If the shipping address is byte-equal to the billing address, the controller stores `null` to avoid emitting a duplicated BG-13 in Factur-X. B2C example: ```bash curl -X POST https://api.scell.io/api/v1/buyers \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "name": "Marie Dupont", "is_individual": true, "country": "FR", "email": "marie.dupont@example.fr", "billing_address": { "line1": "8 Rue Lepic", "postal_code": "75018", "city": "Paris", "country": "FR" } }' ``` #### `GET /api/v1/buyers/{buyer}` ```bash curl https://api.scell.io/api/v1/buyers/0193af1a-… \ -H 'X-API-Key: sk_live_…' ``` `404` if outside the actor's `(tenant, sub_tenant)` scope (anti-IDOR). #### `PATCH /api/v1/buyers/{buyer}` and `PUT /api/v1/buyers/{buyer}` Same payload as `POST`. Updates the registry **only** — never alters historical invoices that snapshotted this buyer. #### `DELETE /api/v1/buyers/{buyer}` Soft delete. Returns `204`. The buyer disappears from `GET /v1/buyers` listings but remains referenced by historical invoices via their `buyer_id` snapshot. ### 5.3 Companies — `/v1/companies` Sanctum-only (dashboard). #### `GET /api/v1/companies` List of companies the tenant owns. ```bash curl https://api.scell.io/api/v1/companies \ -H 'Authorization: Bearer 1|abc...' ``` #### `POST /api/v1/companies` ```bash curl -X POST https://api.scell.io/api/v1/companies \ -H 'Authorization: Bearer 1|abc...' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme SAS", "siret": "12345678901234", "vat_number": "FR12345678901", "legal_form": "SAS", "address_line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR", "email": "billing@acme.fr", "iban": "FR7630006000011234567890189", "bic": "BNPAFRPPXXX", "payment_terms_default": "Paiement a 30 jours fin de mois.", "payment_due_days_default": 30, "invoice_footer_default": "SAS au capital de 100 000 EUR — RCS Paris 123 456 789", "invoice_notes_default": "Conformément à la loi française, en cas de retard de paiement..." }' ``` Returns `201`. The resulting company is referenced by invoices via `seller_company_id` (legacy tenant-direct flow) or implicitly via the authenticated user's selected company. #### `GET /api/v1/companies/{id}` #### `PUT /api/v1/companies/{id}` #### `DELETE /api/v1/companies/{id}` #### `POST /api/v1/companies/{id}/kyc` Initiates KYB via SUPER PDP. #### `GET /api/v1/companies/{id}/kyc/status` Returns the current KYC status. ### 5.4 Invoices — `/v1/invoices` Shared surface (FlexibleApiAuth). `sk_*` and Sanctum cookie both work. #### `GET /api/v1/invoices` | Param | Type | Description | |----------------|---------|-----------------------------------------------------------| | `direction` | enum | `outgoing`, `incoming`. Default `outgoing`. | | `status` | enum | Filter by status. | | `buyer_id` | uuid | Restrict to a registered buyer. | | `from` | date | `issue_date >=`. | | `to` | date | `issue_date <=`. | | `q` | string | Search on `invoice_number`, `buyer_name`. | | `per_page` | integer | Default 25, max 100. | | `page` | integer | | ```bash curl 'https://api.scell.io/api/v1/invoices?status=paid&from=2026-01-01' \ -H 'X-API-Key: sk_live_…' ``` #### `POST /api/v1/invoices` — flat flow Body schema = §4.4 + invoice line schema. All snake_case. ##### B2B with shipping address ```bash curl -X POST https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "invoice_date": "2026-05-06", "due_date": "2026-06-05", "direction": "outgoing", "output_format": "facturx", "currency": "EUR", "seller_name": "Acme SAS", "seller_country": "FR", "seller_siret": "12345678901234", "seller_vat_number": "FR12345678901", "seller_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_name": "Globex SAS", "buyer_country": "FR", "buyer_siret": "98765432109876", "buyer_vat_number": "FR98765432109", "buyer_address": { "line1": "10 Place Vendome", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_shipping_address": { "name": "Globex Entrepot Saint-Ouen", "line1": "42 Rue des Rosiers", "postal_code": "93400", "city": "Saint-Ouen", "region": "Ile-de-France", "country": "FR" }, "total_ht": 1000.00, "total_tva": 200.00, "total_ttc": 1200.00, "lines": [ { "description": "Prestation de conseil — mai 2026", "quantity": 5, "unit": "DAY", "unit_price_ht": 200.00, "tva_rate": 20, "total_ht": 1000.00, "total_ttc": 1200.00 } ], "payment_terms": "Paiement a 30 jours fin de mois.", "notes": "Numero de bon de commande: PO-2026-00042" }' ``` ##### B2C particulier ```bash curl -X POST https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "invoice_date": "2026-05-06", "due_date": "2026-05-06", "direction": "outgoing", "output_format": "facturx", "currency": "EUR", "seller_name": "Acme SAS", "seller_country": "FR", "seller_siret": "12345678901234", "seller_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_name": "Marie Dupont", "buyer_country": "FR", "buyer_is_individual": true, "buyer_address": { "line1": "8 Rue Lepic", "postal_code": "75018", "city": "Paris", "country": "FR" }, "total_ht": 50.00, "total_tva": 10.00, "total_ttc": 60.00, "lines": [ { "description": "Service ponctuel", "quantity": 1, "unit_price_ht": 50.00, "tva_rate": 20, "total_ht": 50.00, "total_ttc": 60.00 } ] }' ``` ##### Buyer registry shortcut (recommended) ```bash curl -X POST https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "invoice_date": "2026-05-06", "due_date": "2026-06-05", "direction": "outgoing", "output_format": "facturx", "currency": "EUR", "seller_name": "Acme SAS", "seller_country": "FR", "seller_siret": "12345678901234", "seller_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_id": "0193af1a-7c64-7b9b-94e3-dbcfe4ff0001", "total_ht": 1000.00, "total_tva": 200.00, "total_ttc": 1200.00, "lines": [ { "description": "Conseil", "quantity": 5, "unit_price_ht": 200, "tva_rate": 20, "total_ht": 1000, "total_ttc": 1200 } ] }' ``` When `buyer_id` is set, all flat `buyer_*` fields are optional. The controller copies the registry's current state to the invoice as a snapshot. Mutating that registry buyer later does not change the invoice. ##### Standalone deposit invoice (v2.15.0+) Create deposit invoices (Factur-X type 386) without a parent quote. Specify `invoice_type: "deposit"` + `deposit_total_ht` (deal total) + optional `deposit_reference_text` (free-text reference). ```bash # First deposit — creates a new group curl -X POST https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "invoice_type": "deposit", "deposit_total_ht": 10000.00, "deposit_reference_text": "Proposition commerciale signee le 15/05/2026", "invoice_date": "2026-05-15", "due_date": "2026-05-30", "direction": "outgoing", "output_format": "facturx", "total_ht": 3000.00, "total_tva": 600.00, "total_ttc": 3600.00, "seller_name": "Acme SAS", "seller_siret": "12345678901234", "seller_country": "FR", "seller_address": { "line1": "1 Rue X", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_name": "Client SA", "buyer_siret": "98765432109876", "buyer_country": "FR", "buyer_address": { "line1": "2 Av Y", "postal_code": "69001", "city": "Lyon", "country": "FR" }, "lines": [{ "description": "Acompte 30%", "quantity": 1, "unit_price_ht": 3000, "tva_rate": 20, "total_ht": 3000, "total_ttc": 3600 }] }' ``` Response includes `deposit_group_id` (= invoice ID for the first deposit). ```bash # Subsequent deposit — joins existing group curl -X POST https://api.scell.io/api/v1/invoices \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "invoice_type": "deposit", "deposit_group_id": "0193af1a-…", "invoice_date": "2026-06-15", "direction": "outgoing", "output_format": "facturx", "total_ht": 7000.00, "total_tva": 1400.00, "total_ttc": 8400.00, ... (same seller/buyer/lines) }' ``` When `total_ht` of the new deposit equals the remaining amount of the group, the invoice is **auto-converted** to a balance (type 380) with `parent_invoice_ids` auto-filled and Factur-X BG-22 deductions generated. **New optional fields on `POST /api/v1/invoices`:** | Field | Type | Description | |---|---|---| | `invoice_type` | `"standard"` \| `"deposit"` \| `"balance"` | Default `"standard"` | | `deposit_group_id` | `uuid` | UUID of first deposit in group (null = new group) | | `deposit_total_ht` | `number` | Total deal HT (required for first deposit of a new group) | | `deposit_reference_text` | `string` | Free-text reference (max 1000 chars) | **New fields on `GET /api/v1/invoices/{id}` response:** | Field | Type | Description | |---|---|---| | `deposit_group_id` | `uuid\|null` | Group anchor ID | | `deposit_total_ht` | `number\|null` | Deal total HT | | `deposit_reference_text` | `string\|null` | Free-text ref | | `deposit_group_progress` | `object\|null` | `{ deposit_total_ht, sum_deposits_ht, remaining_ht, progress_percent, has_balance, invoices_count, invoices[] }` | #### `GET /api/v1/invoices/{id}` ```bash curl https://api.scell.io/api/v1/invoices/0193af1a-… \ -H 'X-API-Key: sk_live_…' ``` #### `PUT /api/v1/invoices/{id}` Only allowed in `draft` status. Trying to mutate the immutable fiscal fields on a non-draft invoice raises `RuntimeException` (ISCA guard) and returns `422` with details. #### `DELETE /api/v1/invoices/{id}` **Forbidden in production.** Even drafts: the policy and middleware reject the call with `403`. Invoices are kept indefinitely for fiscal compliance. Use the dashboard cancellation flow if needed. #### `POST /api/v1/invoices/{id}/send-by-email` Sends the generated Factur-X PDF to the buyer by email. The endpoint resolves the recipient address using this cascade (stops at the first found): 1. Explicit `email` field in the request body. 2. `buyer.billing_email` on the linked Buyer registry entry. 3. `buyer.email` on the linked Buyer registry entry. 4. `buyer_email` snapshot column on the invoice itself. Returns `422 BUYER_HAS_NO_EMAIL` when no email address is found through any of the four paths. ```bash # 1. Let the cascade resolve the email automatically curl -X POST https://api.scell.io/api/v1/invoices/0193af1a-…/send-by-email \ -H 'X-API-Key: sk_live_…' # 2. Override with an explicit recipient curl -X POST https://api.scell.io/api/v1/invoices/0193af1a-…/send-by-email \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{"email": "compta@acme.fr"}' ``` Error when no email is resolvable: ```json { "message": "No email address found for this buyer.", "code": "BUYER_HAS_NO_EMAIL" } ``` The call returns `200` with `{ "sent_to": "compta@acme.fr" }` on success. The invoice must be in a non-draft status (validated, transmitted, paid, etc.). Sending a draft returns `422`. #### `POST /api/v1/invoices/{id}/submit` Validates and queues the invoice for transmission (PDP, Chorus, Peppol depending on tenant config). Idempotent — calling twice returns the same `transmitted_at`. ```bash curl -X POST https://api.scell.io/api/v1/invoices/0193af1a-…/submit \ -H 'X-API-Key: sk_live_…' ``` #### `POST /api/v1/invoices/{id}/mark-paid` Mark an outgoing invoice as paid (manual payment). Accepts invoices in status `validated`, `transmitted`, `sent`, or `accepted`. Sets `status=paid`, `paid_at=now()`, `payment_method=manual`. Returns 422 `INVOICE_NOT_PAYABLE` if the status does not allow this transition. ```bash curl -X POST https://api.scell.io/api/v1/invoices/0193af1a-…/mark-paid \ -H 'X-API-Key: sk_live_…' ``` #### `GET /api/v1/invoices/{id}/audit-trail` Returns the immutable trail of state changes for an invoice (creation, validation, transmission, status updates, etc.). Used by ISCA auditors. ```bash curl https://api.scell.io/api/v1/invoices/0193af1a-…/audit-trail \ -H 'X-API-Key: sk_live_…' ``` #### `GET /api/v1/invoices/{id}/download` (and `…/download/{type}`) Two equivalent shapes: ```bash # 1. Path style — type ∈ original | converted | pdf curl https://api.scell.io/api/v1/invoices/0193af1a-…/download/pdf \ -H 'X-API-Key: sk_live_…' \ -o invoice.pdf # 2. Query style — format ∈ facturx | xml | pdf curl 'https://api.scell.io/api/v1/invoices/0193af1a-…/download?format=facturx' \ -H 'X-API-Key: sk_live_…' \ -o invoice-facturx.pdf ``` Returns the raw file with `Content-Type: application/pdf` or `application/xml`. #### `POST /api/v1/invoices/convert` Multipart upload of a PDF or XML file and conversion to a target format. ```bash curl -X POST https://api.scell.io/api/v1/invoices/convert \ -H 'X-API-Key: sk_live_…' \ -F 'original_file=@./invoice.pdf' \ -F 'output_format=facturx' ``` #### `GET /api/v1/invoices/{invoice}/remaining-creditable` Returns the remaining amount that can still be credited via credit notes on this invoice. Useful before `POST /credit-notes`. ```bash curl https://api.scell.io/api/v1/invoices/0193af1a-…/remaining-creditable \ -H 'X-API-Key: sk_live_…' ``` #### Bulk operations (Sanctum only) `POST /api/v1/invoices/bulk-status` and `POST /api/v1/invoices/bulk-submit` accept `{ "ids": ["…", "…"] }` arrays. Dashboard-only — no SDK use case documented. #### Incoming invoices (factures fournisseurs) Sanctum dashboard family rooted at `/v1/invoices/incoming/*`: | Method | Path | Effect | |---------|-----------------------------------------------|-----------------------------------------| | GET | `/v1/invoices/incoming` | List supplier invoices. | | GET | `/v1/invoices/incoming/{invoice}` | Show. | | POST | `/v1/invoices/incoming/{invoice}/accept` | Mark accepted (DGFiP cycle). | | POST | `/v1/invoices/incoming/{invoice}/reject` | Reject with reason. | | POST | `/v1/invoices/incoming/{invoice}/dispute` | Dispute. | | POST | `/v1/invoices/incoming/{invoice}/mark-paid` | Mark paid. | | POST | `/v1/invoices/incoming/{invoice}/mark-read` | Idempotent read receipt. | | GET | `/v1/invoices/incoming/{invoice}/download` | Download original / Factur-X. | ### 5.5 Credit notes — `/v1/credit-notes` Sanctum-only. (Tenant SDK surface lives at `/v1/tenant/credit-notes`, see §5.14.) #### `GET /api/v1/credit-notes` List paginated. #### `POST /api/v1/credit-notes` ```bash curl -X POST https://api.scell.io/api/v1/credit-notes \ -H 'Authorization: Bearer 1|abc...' \ -H 'Content-Type: application/json' \ -d '{ "invoice_id": "0193af1a-…", "type": "partial", "reason": "Erreur de quantite sur la ligne 2", "issue_date": "2026-05-06", "currency": "EUR", "items": [ { "description": "Remboursement partiel", "quantity": 1, "unit_price": 100.00, "vat_rate": 20 } ] }' ``` The controller inherits buyer / seller / `buyer_is_individual` from the parent invoice. Trying to override those fields → `422`. #### `GET /api/v1/credit-notes/{id}` #### `PUT /api/v1/credit-notes/{id}` — drafts only. #### `POST /api/v1/credit-notes/{creditNote}/send` (alias `…/submit`) Locks the credit note and emits it. Idempotent. #### `GET /api/v1/credit-notes/{creditNote}/download` Returns the Factur-X PDF. #### `DELETE /api/v1/credit-notes/{id}` Drafts only. Sent credit notes throw `RuntimeException` from the model boot guard. ### 5.6 Signatures — `/v1/signatures` `sk_*` only for `POST` (cost balance debited). `GET` family is shared with Sanctum dashboard. #### `POST /api/v1/signatures` ```bash curl -X POST https://api.scell.io/api/v1/signatures \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "contract-2026-042", "document": { "file_url": "https://my-storage/contract.pdf", "file_name": "contract.pdf" }, "signers": [ { "email": "alice@globex.fr", "first_name": "Alice", "last_name": "Martin", "phone": "+33612345678", "language": "fr", "auth_mode": "sms", "message": "Bonjour Alice, voici votre contrat. Code OTP: {OTP}" } ], "signature_positions": [ { "signer_index": 0, "page": 3, "x": 50, "y": 80, "width": 30, "height": 8, "unit": "percent" } ], "signature_options": { "signature_mode": "drawn", "signer_must_read": true, "timezone": "Europe/Paris" }, "ui_config": { "iframe_ancestors": ["https://app.acme.fr"], "sidebar_color": "#0F172A", "header_color": "#1E293B" }, "redirect_url": "https://app.acme.fr/contracts/42/done", "webhook_url": "https://api.acme.fr/webhooks/signature" }' ``` Response includes the signature `id` and `signed_url` to redirect the signer to. #### `GET /api/v1/signatures` (Sanctum) #### `GET /api/v1/signatures/{id}` (Sanctum) #### `GET /api/v1/signatures/{id}/download/{type}` — `signed_pdf`, `audit_trail`. #### `POST /api/v1/signatures/{id}/remind` — sends a reminder email/SMS. #### `POST /api/v1/signatures/{id}/cancel` — cancels a pending signature. #### `ui_config` — 21 visual customisation fields All 21 fields are optional. When omitted the backend fills them with the Scell.io palette. Explicit values are preserved; only missing keys receive defaults. | Field | Type | Description | |------------------------------|----------|----------------------------------------------------| | `sidebar_color` | string | Hex color for the sidebar background. | | `sidebar_text_color` | string | Hex color for sidebar text. | | `sidebar_logo_url` | string | URL of the logo shown in the sidebar. | | `sidebar_logo_alt` | string | Alt text for the sidebar logo. | | `header_color` | string | Hex color for the top header bar. | | `header_text_color` | string | Hex color for header text. | | `footer_color` | string | Hex color for the footer bar. | | `footer_text_color` | string | Hex color for footer text. | | `footer_text` | string | Footer text content (e.g. company name + legal). | | `button_color` | string | Hex color for primary action buttons. | | `button_text_color` | string | Hex color for button labels. | | `sign_button_color` | string | Hex color specifically for the sign/submit button. | | `sign_button_text_color` | string | Hex color for the sign button label. | | `hide_scell_branding` | boolean | When `true`, hides the "Powered by Scell.io" mark. | | `hide_progress_bar` | boolean | When `true`, hides the signing progress indicator. | | `hide_signer_list` | boolean | When `true`, hides the list of signers. | | `iframe_ancestors` | string[] | Allowed origins for iframe embedding (CSP). Auto-extended with `https://sign.scell.io`. Max 20 entries.| `iframe_ancestors` is always deduplicated and augmented with `https://sign.scell.io` and `https://scell.io` so the Scell wrapper page can embed the upstream iframe regardless of caller configuration. #### `signature_options` — signing behaviour | Field | Type | Allowed values | Default | Description | |----------------------|---------|-----------------------------|--------------|---------------------------------------------------| | `signature_mode` | enum | `typed`, `drawn`, `both` | `both` | How signers can draw their signature. | | `signer_must_read` | boolean | | `false` | Forces the signer to scroll to the bottom. | | `user_editable_data` | boolean | | `false` | Lets signers edit their own name/initials. | | `timezone` | string | IANA timezone | `Europe/Paris` | Timezone for date rendering in the signing UI. | #### `signature_positions[].unit` — pixel vs percent | Value | Coordinates interpretation | |-----------|-------------------------------------------------------------------| | `percent` | `x`, `y`, `width`, `height` in % of page size. **Default.** | | `pixel` | Absolute pixels on the rendered page. Requires `page_width_px` + `page_height_px` when auto-detection via PDF parse may fail (scanned PDFs). | When `unit: "pixel"` is sent without `page_width_px` / `page_height_px`, the backend attempts automatic PDF dimension detection via `smalot/pdfparser`. On failure it falls back to A4 (595 × 842 pt). Provide explicit dimensions to guarantee accuracy. #### `signature_positions[].signer_index` — assign positions to a signer | Field | Type | Required | Range | Description | |----------------|---------|----------|-------|----------------------------------------------------| | `signer_index` | integer | no | 0..9 | 0-based index referencing a signer in `signers[]`. | EU-SES allows **several signature positions per signer**. Each entry in `signature_positions[]` carries its own `signer_index`, so the same signer can have N signature boxes (e.g. one on every page, or initials + final signature). Repeat an entry with the same `signer_index` for each box. When `signer_index` is omitted on **all** positions, the legacy positional mapping applies: position `i` is bound to `signers[i]` (one position per signer). Mixing both styles in the same request is not recommended — set `signer_index` explicitly on every position once you need multiple boxes. ```jsonc { "signers": [ { "email": "alice@globex.fr", "first_name": "Alice", "last_name": "Martin", "auth_mode": "email" }, { "email": "bob@globex.fr", "first_name": "Bob", "last_name": "Durand", "auth_mode": "email" } ], "signature_positions": [ { "signer_index": 0, "page": 1, "x": 50, "y": 80, "unit": "percent" }, { "signer_index": 0, "page": 3, "x": 50, "y": 80, "unit": "percent" }, { "signer_index": 1, "page": 3, "x": 50, "y": 60, "unit": "percent" } ] } ``` In this example Alice (`signer_index: 0`) signs on pages 1 and 3, while Bob (`signer_index: 1`) signs once on page 3. #### `signers[].message` placeholder Custom per-signer messages (max 500 chars) support the `{OTP}` placeholder. The upstream EU-SES provider replaces it with the one-time password when `auth_mode: "sms"` is active. ```jsonc { "signers": [{ "email": "alice@globex.fr", "first_name": "Alice", "last_name": "Martin", "phone": "+33612345678", "auth_mode": "sms", "message": "Bonjour Alice, merci de signer le contrat. Votre code : {OTP}" }] } ``` ### 5.7 Webhooks — `/v1/webhooks` Sanctum-only. ```bash curl -X POST https://api.scell.io/api/v1/webhooks \ -H 'Authorization: Bearer 1|abc...' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://api.acme.fr/webhooks/scell", "events": ["invoice.created", "invoice.transmitted", "invoice.paid", "signature.signed"], "description": "Production webhook" }' ``` Returns the webhook with its `secret`. Save it now — subsequent reads return only the prefix. | Method | Path | Effect | |--------|----------------------------------------|-------------------------------------------------| | GET | `/v1/webhooks` | List. | | POST | `/v1/webhooks` | Create. | | GET | `/v1/webhooks/{id}` | Show. | | PUT | `/v1/webhooks/{id}` | Update url / events / is_active. | | DELETE | `/v1/webhooks/{id}` | Remove. | | POST | `/v1/webhooks/{id}/regenerate-secret` | Rotate the HMAC secret. Returns the new secret. | | POST | `/v1/webhooks/{id}/test` | Sends a `webhook.test` event. | | GET | `/v1/webhooks/{id}/logs` | Last 100 deliveries with status code + latency. | ### 5.8 API keys — `/v1/api-keys` Sanctum-only. ```bash # List curl https://api.scell.io/api/v1/api-keys \ -H 'Authorization: Bearer 1|abc...' # Create — returns the full secret ONCE curl -X POST https://api.scell.io/api/v1/api-keys \ -H 'Authorization: Bearer 1|abc...' \ -H 'Content-Type: application/json' \ -d '{"name":"Backend prod","environment":"live","scopes":["*"]}' # Show curl https://api.scell.io/api/v1/api-keys/0193... \ -H 'Authorization: Bearer 1|abc...' # Revoke curl -X DELETE https://api.scell.io/api/v1/api-keys/0193... \ -H 'Authorization: Bearer 1|abc...' ``` `update` is intentionally not exposed — to rotate, revoke and create anew. ### 5.9 Billing & balance — `/v1/tenant/billing/*`, `/v1/tenant/balance` > The standalone `/v1/balance/*` routes were **removed** (they now return > 404). Balance, top-up, usage and transaction history live under the > `/v1/tenant/billing/*` family, accepted both via Sanctum cookie (dashboard) > and `X-API-Key` (`sk_*`). The read-only balance summary also remains at > `/v1/tenant/balance` (legacy surface, §5.14). | Method | Path | Effect | |--------|-------------------------------------------------------|-------------------------------| | GET | `/v1/tenant/balance` | Current tenant balance. | | GET | `/v1/tenant/billing/usage` | Consumption report. | | GET | `/v1/tenant/billing/transactions` | Paginated transaction history.| | GET | `/v1/tenant/billing/top-up/preview` | Bonus % preview (no PI). | | POST | `/v1/tenant/billing/top-up` | Manual top-up (Stripe PI). | | POST | `/v1/tenant/billing/top-up/confirm` | Confirm Stripe payment. | | GET | `/v1/tenant/billing/invoices` | Monthly Scell.io invoices. | | GET | `/v1/tenant/billing/invoices/{invoice}` | Show. | | GET | `/v1/tenant/billing/invoices/{invoice}/download` | Download PDF. | | POST | `/v1/tenant/billing/invoices/{invoice}/pay` | Pay an open Scell.io invoice. | | GET | `/v1/tenant/billing/packs` | Active prepaid credit packs. | | POST | `/v1/tenant/billing/packs/{packSlug}/checkout` | Buy a credit pack. | ```bash # Current balance (server-to-server) curl https://api.scell.io/api/v1/tenant/balance \ -H 'X-API-Key: sk_live_a2c4e6…' ``` A fuller listing of the billing surface (payment methods, Scell.io invoices) is in §5.14. ### 5.10 Invoice templates — `/v1/tenant/invoice-templates` Tenant key surface (`X-Tenant-Key`). Manages invoice/credit-note PDF personalisation. ```bash # List curl https://api.scell.io/api/v1/tenant/invoice-templates \ -H 'X-Tenant-Key: sk_live_…' # Create curl -X POST https://api.scell.io/api/v1/tenant/invoice-templates \ -H 'X-Tenant-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme corporate", "scope": "tenant", "logo_url": "https://cdn.acme.fr/logo.png", "palette": { "primary": "#0F172A", "secondary": "#F59E0B" }, "footer_html": "

SAS au capital de 100 000 EUR — RCS Paris 123 456 789

" }' # Mark default curl -X PUT https://api.scell.io/api/v1/tenant/invoice-templates/0193af1a-…/default \ -H 'X-Tenant-Key: sk_live_…' # Upload logo (multipart) curl -X POST https://api.scell.io/api/v1/tenant/invoice-templates/0193af1a-…/logo \ -H 'X-Tenant-Key: sk_live_…' \ -F 'logo=@./logo.png' ``` Cascade on invoice generation: explicit `invoice_template_id` > sub_tenant default > tenant default > system default. Cached in Redis 5 minutes. ### 5.11 Sub-tenants — `/v1/tenant/sub-tenants` Tenant key surface. Used by partner/marketplace tenants. ```bash # List curl https://api.scell.io/api/v1/tenant/sub-tenants \ -H 'X-Tenant-Key: sk_live_…' # Lookup by external id (your partner's internal id) curl https://api.scell.io/api/v1/tenant/sub-tenants/by-external-id/merchant-42 \ -H 'X-Tenant-Key: sk_live_…' # Create curl -X POST https://api.scell.io/api/v1/tenant/sub-tenants \ -H 'X-Tenant-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "merchant-42", "name": "Merchant 42", "siret": "11122233344455", "country": "FR", "contact_email": "ops@merchant-42.fr", "address": { "line1": "1 Avenue Marchande", "postal_code": "75002", "city": "Paris", "country": "FR" } }' # Show / Update curl https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-… \ -H 'X-Tenant-Key: sk_live_…' curl -X PUT https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-… ... # Delete (compliance ISCA — voir politique ci-dessous) curl -X DELETE https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-… \ -H 'X-Tenant-Key: sk_live_…' # Avec cascade des Companies orphelines (sans facture emise) : curl -X DELETE 'https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-…?cascade=true' \ -H 'X-Tenant-Key: sk_live_…' # Stats curl https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-…/stats/overview \ -H 'X-Tenant-Key: sk_live_…' # SuperPDP — refresh statut (1/min, rate-limited) curl -X POST https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-…/superpdp-status/refresh \ -H 'X-Tenant-Key: sk_live_…' # SuperPDP — genere une URL d'authorize pour lancer/relancer la connexion curl -X POST https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-…/superpdp-authorize \ -H 'X-Tenant-Key: sk_live_…' # SuperPDP — signed URL de reprise du tunnel (7j TTL) curl -X POST https://api.scell.io/api/v1/tenant/sub-tenants/0193af1a-…/resume-url \ -H 'X-Tenant-Key: sk_live_…' ``` #### DELETE — politique ISCA (compliance fiscale) 3 scenarios via le champ `code` de la reponse 422 : | Code (422) | Cause | Resolution | |-----------------------------------|---------------------------------------------|--------------------------------------------------------------| | `SUB_TENANT_HAS_COMPANIES` | Companies rattachees sans facture emise | Relancer avec `?cascade=true` | | `SUB_TENANT_HAS_FISCAL_ENTRIES` | Invoice ou CreditNote emise existante | Refus systematique (ledger immutable). Preferer `PUT { is_active: false }` | Reponse `SUB_TENANT_HAS_COMPANIES` : ```json { "error": "Ce sub-tenant a des entreprises rattachees mais aucune facture emise. ...", "code": "SUB_TENANT_HAS_COMPANIES", "companies_count": 2 } ``` Succes (200) avec cascade : ```json { "message": "Sub-tenant supprime", "companies_deleted": 2 } ``` #### POST `/superpdp-status/refresh` — payload 422 enrichi Si le sub-tenant n'a pas d'`access_token` SuperPDP (cree manuellement sans widget v3), la reponse 422 inclut l'URL OAuth a ouvrir : ```json { "error": "Aucun access_token SuperPDP disponible pour ce sub-tenant.", "code": "MISSING_ACCESS_TOKEN", "authorize_url": "https://oauth.superpdp.tech/authorize?response_type=code&...", "state": "abc123...", "message": "Aucune connexion SuperPDP active pour ce sub-tenant. Ouvrez l'authorize_url pour lancer le tunnel OAuth." } ``` Le `state` + `code_verifier` (PKCE) sont persistes en cache 1h cote serveur pour validation au callback OAuth. Le frontend doit ouvrir `authorize_url` dans le navigateur de l'utilisateur final (ex: `window.open(url, '_blank')`). #### Vue admin cross-tenant — `/v1/admin/sub-tenants` Reserve aux administrateurs Scell.io. Retourne les Tenants (paginees) avec leurs SubTenants en eager load pour construire une vue arborescente. ```bash # Liste arborescente Tenants -> sub_tenants[] curl https://api.scell.io/api/v1/admin/sub-tenants?search=acme \ -H 'Cookie: scellio-session=...' # Detail cross-tenant (sub-tenant + tenant parent + stats) curl https://api.scell.io/api/v1/admin/sub-tenants/0193af1a-… \ -H 'Cookie: scellio-session=...' # Delete cross-tenant (meme politique ISCA + cascade) curl -X DELETE 'https://api.scell.io/api/v1/admin/sub-tenants/0193af1a-…?cascade=true' \ -H 'Cookie: scellio-session=...' ``` #### Polling automatique du KYB SuperPDP Le scheduler interne poll `GET /v1.beta/oauth2_sessions/me` toutes les **15 minutes** pour les sub-tenants en attente de verification (`pending_superpdp` / `superpdp_redirected` / `superpdp_authorized` / `superpdp_pending_review`). Pas besoin d'appeler `refresh` manuellement pour les sub-tenants encore en cours d'onboarding. ### 5.12 Fiscal compliance — `/v1/tenant/fiscal/*` (read-only from SDK) Tenant key + scope `fiscal:read`. Write/admin scopes are not exposed to the SDK in normal flows. | Path | Description | |-------------------------------------------------------|-----------------------------------------------------| | `GET /v1/tenant/fiscal/compliance` | Snapshot of compliance health. | | `GET /v1/tenant/fiscal/integrity` | Live ISCA chain integrity check. | | `GET /v1/tenant/fiscal/integrity-status` | Cached integrity status. | | `GET /v1/tenant/fiscal/integrity/history` | Per-day integrity history. | | `GET /v1/tenant/fiscal/integrity/{date}` | Integrity for a specific date. | | `GET /v1/tenant/fiscal/closings` | List daily/monthly closings. | | `GET /v1/tenant/fiscal/entries` | Paginated immutable ledger entries. | | `GET /v1/tenant/fiscal/anchors` | TSA + OpenTimestamps proofs. | | `GET /v1/tenant/fiscal/rules` | LF 2026 fiscal rules. | | `GET /v1/tenant/fiscal/rules/{key}` | Show rule details. | | `GET /v1/tenant/fiscal/rules/{key}/history` | Rule history. | | `GET /v1/tenant/fiscal/rules/export` | Export ruleset (JSON). | | `GET /v1/tenant/fiscal/fec` | FEC export for French tax authorities. | | `GET /v1/tenant/fiscal/forensic-export` | Comprehensive forensic dump. | | `GET /v1/tenant/fiscal/attestation/{year}` | Annual auto-attestation (preview). | | `GET /v1/tenant/fiscal/attestation/{year}/download` | Annual auto-attestation (PDF). | | `GET /v1/tenant/fiscal/kill-switch/status` | Kill-switch state. | | `GET /v1/tenant/fiscal/closings/{closing}/csv` | Daily closure CSV (signed URL, 5-day expiry). | | `GET /v1/tenant/fiscal/isca/measures-register/download` | Mandatory measures register PDF. | | `GET /v1/tenant/fiscal/isca/technical-dossier/download`| Technical dossier PDF. | | `GET /v1/tenant/fiscal/isca/self-attestation/download` | Tenant self-attestation PDF. | | `GET /v1/tenant/fiscal/isca/self-attestation/{subTenantId}/download` | Sub-tenant self-attestation. | ```bash curl https://api.scell.io/api/v1/tenant/fiscal/integrity \ -H 'X-Tenant-Key: sk_live_…' ``` Response: ```json { "data": { "as_of": "2026-05-06T08:00:00+00:00", "chains": [ { "scope": { "tenant_id": "01...", "sub_tenant_id": null }, "head_sequence": 12482, "head_hash": "a3f1c9...e472", "gaps": [], "orphan_entities": [], "tampering": false } ] } } ``` ### 5.13 Onboarding & SuperPDP — `/v1/onboarding/*` Public surface for partner widget integration. `pk_*` or `sk_*` accepted. #### `POST /api/v1/onboarding/sessions` ```bash curl -X POST https://api.scell.io/api/v1/onboarding/sessions \ -H 'X-Publishable-Key: pk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "merchant-42", "redirect_url": "https://app.merchant-42.fr/onboarding/done", "prefill": { "name": "Merchant 42", "email": "ops@merchant-42.fr" } }' ``` Returns a session id and the SuperPDP authorize URL to redirect the end-user to. #### `GET /api/v1/onboarding/sessions/{id}` Polls the session status. Returns the resulting `sub_tenant_id` once KYB is verified. #### `POST /api/v1/onboarding/superpdp/authorize` Starts the OAuth 2.1 authorization_code + PKCE flow toward SUPER PDP. #### `POST /api/v1/onboarding/superpdp/callback` Consumes the OAuth code and creates the SubTenant with encrypted SUPER PDP tokens. Used by the widget v1 (legacy POST flow). #### `POST /api/v1/me/superpdp/authorize` (Sanctum) Tenant admin self-onboarding to SUPER PDP. #### `GET /api/v1/me/superpdp/callback` Browser redirect target post-OAuth. #### `GET /api/v1/widget/oauth-callback` Widget v2 redirect URI. Posts `sub_tenant` + credentials back to the parent window via `postMessage`. #### `POST /api/v1/onboarding/exchange` (and legacy `/v1/tenant/onboarding/exchange`) Public — uses tenant `api_key` in body. Strict rate limit 5 req/min/IP to prevent brute force. Used by widget v1 to exchange a session code for a final sub_tenant payload. ```bash curl -X POST https://api.scell.io/api/v1/onboarding/exchange \ -H 'Content-Type: application/json' \ -d '{ "api_key": "sk_live_…", "code": "session-code-from-widget" }' ``` ### 5.14 Tenant legacy surface — `/v1/tenant/*` Family historically used by partners before the unified `sk_*` flow. Header: `X-Tenant-Key`. Equivalent to a `sk_*` secret key but with tenant-scoped helpers (sub-tenants, billing, fiscal admin). #### Tenant profile | Method | Path | Description | |--------|------------------------------------------|------------------------------------------| | GET | `/v1/tenant/me` | Tenant identity. | | PUT | `/v1/tenant/me` | Update tenant info. | | GET | `/v1/tenant/balance` | Tenant balance. | | GET | `/v1/tenant/stats` | Aggregated stats. | | GET | `/v1/tenant/stats/overview` | Detailed dashboard stats. | | GET | `/v1/tenant/stats/monthly` | Monthly aggregates. | | POST | `/v1/tenant/regenerate-key` | Rotate the secret key. | | POST | `/v1/tenant/keys/publishable` | Generate `pk_*`. | #### Tenant-direct invoices (no sub-tenant) These accept the legacy `seller_company_id` + `buyer_type` payload (see `StoreTenantDirectInvoiceRequest`). ```bash curl -X POST https://api.scell.io/api/v1/tenant/invoices \ -H 'X-Tenant-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "seller_company_id": "0193af1a-…", "buyer_type": "external", "buyer_name": "Globex SAS", "buyer_country": "FR", "buyer_siret": "98765432109876", "buyer_address": { "street": "10 Place Vendome", "city": "Paris", "postal_code": "75001", "country": "FR" }, "issue_date": "2026-05-06", "due_date": "2026-06-05", "currency": "EUR", "lines": [ { "description": "Consulting", "quantity": 5, "unit_price": 200, "vat_rate": 20 } ], "payment_terms": "Paiement a 30 jours fin de mois.", "bank_details": { "iban": "FR7630006000011234567890189", "bic": "BNPAFRPPXXX" } }' ``` | Method | Path | Description | |--------|-------------------------------------------------------------|--------------------------------------| | POST | `/v1/tenant/invoices` | Create direct invoice. | | POST | `/v1/tenant/invoices/bulk` | Create up to N invoices in one call. | | GET | `/v1/tenant/invoices` | List. | | GET | `/v1/tenant/invoices/{invoiceId}` | Show. | | PUT | `/v1/tenant/invoices/{invoiceId}` | Draft updates only. | | DELETE | `/v1/tenant/invoices/{invoiceId}` | Drafts only. | | POST | `/v1/tenant/invoices/{invoiceId}/submit` | Validate + transmit. | | GET | `/v1/tenant/invoices/{invoiceId}/status` | Lifecycle status. | | POST | `/v1/tenant/invoices/bulk-submit` | Bulk submit. | | POST | `/v1/tenant/invoices/bulk-status` | Bulk status query. | | GET | `/v1/tenant/invoices/{invoiceId}/remaining-creditable` | Creditable amount. | | GET | `/v1/tenant/invoices/{invoiceId}/download` | Download Factur-X PDF/A-3 (default) or pure UBL/CII XML. Query: `?format=facturx\|pdf\|xml`. | Sub-tenant scoped variants (use `sub-tenant` middleware): | Method | Path | Description | |--------|-------------------------------------------------------------|--------------------------------------| | POST | `/v1/tenant/sub-tenants/{subTenantId}/invoices` | Create on behalf of sub-tenant. | | GET | `/v1/tenant/sub-tenants/{subTenantId}/invoices` | List sub-tenant invoices. | | GET | `/v1/tenant/sub-tenants/{subTenantId}/invoices/{invoiceId}/download` | Download (sub-tenant scoped, anti-IDOR). Query: `?format=facturx\|pdf\|xml`. | | POST | `/v1/tenant/sub-tenants/{subTenantId}/invoices/incoming` | Register an incoming supplier invoice (free, no balance).| | GET | `/v1/tenant/sub-tenants/{subTenantId}/invoices/incoming` | List sub-tenant incoming invoices. | Tenant-level incoming operations (no sub-tenant context): | Method | Path | Description | |--------|-------------------------------------------------------------|----------------------------| | GET | `/v1/tenant/invoices/incoming/{invoiceId}` | Show. | | POST | `/v1/tenant/invoices/incoming/{invoiceId}/accept` | Accept. | | POST | `/v1/tenant/invoices/incoming/{invoiceId}/reject` | Reject. | | POST | `/v1/tenant/invoices/incoming/{invoiceId}/mark-paid` | Mark paid. | #### Tenant credit notes | Method | Path | Description | |--------|-------------------------------------------------------------|-----------------------------------| | POST | `/v1/tenant/credit-notes` | Direct credit note. | | GET | `/v1/tenant/credit-notes` | List. | | GET | `/v1/tenant/credit-notes/{creditNoteId}` | Show. | | PUT | `/v1/tenant/credit-notes/{creditNoteId}` | Drafts only. | | POST | `/v1/tenant/credit-notes/{creditNoteId}/send` | Lock + send (alias `/submit`). | | DELETE | `/v1/tenant/credit-notes/{creditNoteId}` | Drafts only. | | GET | `/v1/tenant/credit-notes/{creditNoteId}/download` | Download PDF. | | POST | `/v1/tenant/sub-tenants/{subTenantId}/credit-notes` | Sub-tenant credit note. | | GET | `/v1/tenant/sub-tenants/{subTenantId}/credit-notes` | List. | #### Tenant billing (consolidated invoicing for the tenant itself) | Method | Path | Description | |--------|-------------------------------------------------------|-------------------------------| | GET | `/v1/tenant/billing/invoices` | Monthly Scell.io invoices. | | GET | `/v1/tenant/billing/invoices/{invoice}` | Show. | | GET | `/v1/tenant/billing/invoices/{invoice}/download` | Download PDF. | | GET | `/v1/tenant/billing/usage` | Consumption report. | | POST | `/v1/tenant/billing/top-up` | Manual balance top-up. | | POST | `/v1/tenant/billing/top-up/confirm` | Confirm Stripe payment. | | GET | `/v1/tenant/billing/transactions` | Transaction history. | ### 5.15 SIRENE lookup — `/v1/sirene/{siret}` Wrapper over `recherche-entreprises.api.gouv.fr` (Etalab, free). Response normalised snake_case, aligned to Company/Tenant fields. Cache 24h per SIRET. ```bash curl https://api.scell.io/api/v1/sirene/12345678901234 \ -H 'X-API-Key: sk_live_…' ``` Response: ```json { "data": { "siret": "12345678901234", "siren": "123456789", "name": "ACME SAS", "legal_form": "5710", "naf_code": "6201Z", "vat_number": "FR12123456789", "address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "is_active": true, "registered_at": "2018-04-01" } } ``` ### 5.16 Pricing — `/v1/pricing/public`, `/v1/pricing` | Method | Path | Auth | |--------|-------------------------------------------------------|-----------------------| | GET | `/v1/pricing/public` | Public — no auth. | | GET | `/v1/pricing` | Sanctum or X-Tenant-Key. Returns the effective pricing for the caller (with overrides applied).| | GET | `/v1/tenant/pricing` | X-Tenant-Key. | ```bash curl https://api.scell.io/api/v1/pricing/public ``` ### 5.17 Health check & version ```bash curl https://api.scell.io/api/health # {"status":"ok","timestamp":"2026-05-06T08:42:31+00:00"} ``` Public application version (consumed by monitoring + the ISCA compliance certificate). No auth required. ```bash curl https://api.scell.io/api/v1/version ``` ```json { "version": "1.2.1", "commit_sha": "…", "commit_short": "…", "committed_at": "2026-05-28T…", "environment": "production", "php_version": "8.2.x", "laravel_version": "12.x", "resolved_at": "2026-05-28T…" } ``` ### 5.18 Quotes — `/v1/quotes` Quotes are commercial proposals sent to buyers for acceptance. They sit **outside the ISCA fiscal ledger** — their own append-only audit trail (SHA-256 hash chain, isolated per `(tenant_id, sub_tenant_id)`) tracks every state transition. Accepted quotes can be converted to deposit invoices (type 386) or a final balance invoice (type 380). Auth: `sk_*` (`X-API-Key`) or Sanctum cookie. Public quote viewer is unauthenticated — accessed via a `public_token` (HMAC-signed, 90-day TTL). #### Quote status lifecycle ``` draft → sent → viewed → accepted ↘ refused ↘ expired (validity date passed) (cancel) → cancelled (convert-to-deposit) → converted + Factur-X type 386 invoice (convert-to-balance) → converted + Factur-X type 380 invoice ``` The quote number is generated by Scell.io as `DEV-YYYY-NNNN` (per tenant + year sequence). #### `GET /api/v1/quotes` | Param | Type | Description | |-------------|---------|----------------------------------------------| | `status` | enum | `draft`, `sent`, `viewed`, `accepted`, `refused`, `expired`, `converted`, `cancelled`.| | `buyer_id` | uuid | Filter by registered buyer. | | `from` | date | Issue date >=. | | `to` | date | Issue date <=. | | `q` | string | Search on `quote_number`, `buyer_name`. | | `per_page` | integer | Default 25, max 100. | | `page` | integer | | ```bash curl 'https://api.scell.io/api/v1/quotes?status=sent' \ -H 'X-API-Key: sk_live_…' ``` #### `POST /api/v1/quotes` ```bash curl -X POST https://api.scell.io/api/v1/quotes \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "issue_date": "2026-05-20", "expiry_date": "2026-06-20", "currency": "EUR", "buyer_id": "0193af1a-…", "seller_name": "Acme SAS", "seller_country": "FR", "seller_siret": "12345678901234", "seller_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "lines": [ { "description": "Développement application mobile — phase 1", "quantity": 20, "unit": "DAY", "unit_price": 750.00, "vat_rate": 20, "total_ht": 15000.00, "total_ttc": 18000.00 } ], "notes": "Devis valable 30 jours. Acompte de 30% à la commande.", "callback_url": "https://app.acme.fr/quotes/signed-callback" }' ``` Returns `201` with the created quote including `quote_number`, `status: "draft"`, and `public_token` (for the buyer-facing URL). **Key fields on `POST /api/v1/quotes`:** | Field | Type | Required | Description | |-----------------|----------|:--------:|-----------------------------------------------------------| | `issue_date` | date | yes | YYYY-MM-DD. | | `expiry_date` | date | no | When the quote expires. | | `currency` | string | yes | ISO 4217 (`EUR`). | | `buyer_id` | uuid | optional | Registry shortcut (recommended). | | `buyer_*` | various | conditional | Flat buyer fields (when `buyer_id` is absent). | | `seller_name` | string | yes | Legal name of the issuer. | | `seller_country`| string | yes | ISO alpha-2. | | `seller_siret` | string? | conditional | Required for FR sellers. | | `seller_address`| Address | yes | Factur-X seller address. | | `lines` | Line[] | yes | `min:1`. Same schema as Invoice lines. | | `notes` | string? | no | Free text (max 2000). Displayed in the buyer viewer. | | `callback_url` | string? | no | Full URL the buyer is redirected to after signing or refusing. See below. | | `invoice_template_id` | uuid? | no | PDF personalisation template. | #### `callback_url` — post-signature redirect When `callback_url` is set on a quote, the public buyer viewer (`https://sign.scell.io/quotes/{token}`) redirects the buyer to that URL after a successful signature or a refusal. The redirect appends two query parameters: ``` https://app.acme.fr/quotes/signed-callback?quote_id=DEV-2026-0042&action=signed https://app.acme.fr/quotes/signed-callback?quote_id=DEV-2026-0042&action=refused ``` | Parameter | Values | When | |------------|----------------------|-------------------------------------| | `quote_id` | quote number string | Always. | | `action` | `signed` / `refused` | Reflects the buyer's decision. | This lets you close the widget window, show a thank-you page, or trigger downstream flows without relying on webhooks. HTTPS only; `http://` URLs are rejected with `422`. #### `GET /api/v1/quotes/{id}` #### `PUT /api/v1/quotes/{id}` — draft status only. #### `DELETE /api/v1/quotes/{id}` — draft status only. #### `POST /api/v1/quotes/{id}/send` Locks the quote and emails the `public_token` link to the buyer. Transitions status from `draft` to `sent`. ```bash curl -X POST https://api.scell.io/api/v1/quotes/0193af1a-…/send \ -H 'X-API-Key: sk_live_…' ``` #### `POST /api/v1/quotes/{id}/cancel` Cancels a `sent` or `viewed` quote. Transitions to `cancelled`. #### `POST /api/v1/quotes/{id}/duplicate` Creates a new `draft` quote with the same content. #### `GET /api/v1/quotes/{id}/audit-log` Returns the immutable append-only log of all state transitions and actions. Each entry includes a `sequence_number` and `chain_hash` (SHA-256, isolated per `(tenant_id, sub_tenant_id)`). ```bash curl https://api.scell.io/api/v1/quotes/0193af1a-…/audit-log \ -H 'X-API-Key: sk_live_…' ``` #### `POST /api/v1/quotes/{id}/regenerate-public-link` Generates a new `public_token` (HMAC-signed, 90-day TTL). The previous token is immediately invalidated. #### `POST /api/v1/quotes/{id}/convert-to-deposit` Converts the accepted quote into a **deposit invoice** (Factur-X type 386, `invoice_type: "deposit"`). VAT is immediately due (CGI art. 289). Can be called multiple times for partial deposits. ```bash curl -X POST https://api.scell.io/api/v1/quotes/0193af1a-…/convert-to-deposit \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "percent": 30, "label": "Acompte 30% à la commande", "due_date": "2026-06-01" }' ``` Body: send **either** `percent` (0–100) or `amount` (absolute HT, not both). `label` and `due_date` are optional. | Field | Type | Required | Description | |-------------|---------|:-----------------:|-------------------------------------------------| | `percent` | number | one of percent/amount | Percentage of the quote total HT. | | `amount` | number | one of percent/amount | Fixed HT amount. | | `label` | string? | no | Line description on the deposit invoice. | | `due_date` | date? | no | Payment due date. | Returns `201` with the created Invoice. The invoice `parent_quote_id` is set. The deposit group is automatically tracked via `deposit_group_id`. #### `POST /api/v1/quotes/{id}/convert-to-balance` Converts the quote into the **final balance invoice** (Factur-X type 380, `invoice_type: "balance"`). Can only be called once per quote. VAT is not due on the balance (already collected via deposits). Scell auto-generates Factur-X BG-22 deduction lines referencing all prior deposit invoices. ```bash curl -X POST https://api.scell.io/api/v1/quotes/0193af1a-…/convert-to-balance \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{"due_date": "2026-08-01"}' ``` Returns `201` with the created Invoice (`invoice_type: "balance"`, `parent_invoice_ids: ["…deposit1-id…", "…deposit2-id…"]`). #### `GET /api/v1/quotes/{id}/download` / `POST /api/v1/quotes/{id}/download` Returns the quote PDF. #### Public quote endpoints (unauthenticated) Used by the buyer-facing viewer hosted at `https://sign.scell.io/quotes/{token}`. | Method | Path | Description | |--------|-----------------------------------------------|------------------------------------------------| | GET | `/v1/public/quotes/{token}` | Fetch quote data (marks as `viewed` on first hit). | | POST | `/v1/public/quotes/{token}/sign` | Record buyer acceptance (canvas signature). | | POST | `/v1/public/quotes/{token}/refuse` | Record buyer refusal with optional reason. | | GET | `/v1/public/quotes/{token}/download` | Download quote PDF. | The `public_token` is a HMAC-signed URL parameter valid for 90 days from generation. Tampering returns `403`. #### Admin quote endpoints (Sanctum + admin gate) | Method | Path | Description | |--------|----------------------------------------------|---------------------------------| | GET | `/v1/admin/quotes` | Cross-tenant list. | | GET | `/v1/admin/quotes/{id}` | Show. | | DELETE | `/v1/admin/quotes/{id}` | Force delete. | | GET | `/v1/admin/quotes/{id}/audit-log` | Full audit log. | | GET | `/v1/admin/quotes/{id}/integrity-check` | Verify hash chain integrity. | ### 5.19 Payment schedule — `/v1/quotes/{id}/payment-schedule` A payment schedule lets you break a quote's total into dated payment milestones before issuing actual deposit invoices. Each line can be a **percentage** or a fixed **amount** of the quote's total HT. Auth: same as quotes (`sk_*` or Sanctum). #### `GET /api/v1/quotes/{id}/payment-schedule` Returns all schedule lines for the quote, ordered by `due_date`. ```bash curl https://api.scell.io/api/v1/quotes/0193af1a-…/payment-schedule \ -H 'X-API-Key: sk_live_…' ``` Response: ```json { "data": [ { "id": "0193af2b-…", "quote_id": "0193af1a-…", "label": "Acompte 30% à la commande", "percent": 30, "amount": 4500.00, "due_date": "2026-06-01", "invoice_id": null, "invoiced_at": null, "created_at": "2026-05-20T10:00:00+00:00" }, { "id": "0193af3c-…", "quote_id": "0193af1a-…", "label": "Solde à la livraison", "percent": 70, "amount": 10500.00, "due_date": "2026-09-01", "invoice_id": null, "invoiced_at": null, "created_at": "2026-05-20T10:00:00+00:00" } ] } ``` #### `POST /api/v1/quotes/{id}/payment-schedule` Creates a new schedule line. Send **either** `percent` or `amount`, not both. The sum of all `percent` lines must not exceed 100. ```bash curl -X POST https://api.scell.io/api/v1/quotes/0193af1a-…/payment-schedule \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "label": "Acompte 30% à la commande", "percent": 30, "due_date": "2026-06-01" }' ``` | Field | Type | Required | Description | |------------|---------|:-----------------:|-------------------------------------------------------| | `label` | string | yes | Description shown on the schedule (max 255). | | `percent` | number | one of percent/amount | Percentage of quote total HT (0.01–100). | | `amount` | number | one of percent/amount | Absolute HT amount. | | `due_date` | date | yes | YYYY-MM-DD payment target. | Returns `201` with the created line. #### `PATCH /api/v1/quotes/{id}/payment-schedule/lines/{lineId}` Updates an existing line. Only allowed if the line has not yet been converted to an invoice (`invoice_id` is null). ```bash curl -X PATCH https://api.scell.io/api/v1/quotes/0193af1a-…/payment-schedule/lines/0193af2b-… \ -H 'X-API-Key: sk_live_…' \ -H 'Content-Type: application/json' \ -d '{"due_date": "2026-06-15", "label": "Acompte 30% — remis à date"}' ``` #### `DELETE /api/v1/quotes/{id}/payment-schedule/lines/{lineId}` Deletes a schedule line. Returns `204`. Blocked if the line is already invoiced (`422 SCHEDULE_LINE_ALREADY_INVOICED`). #### `POST /api/v1/quotes/{id}/payment-schedule/lines/{lineId}/convert-to-invoice` Converts a schedule line into a real deposit invoice. Internally calls `convert-to-deposit` with the line's `amount` / `percent` and `due_date`. Marks the line as invoiced (`invoice_id` set, `invoiced_at` timestamped). ```bash curl -X POST \ https://api.scell.io/api/v1/quotes/0193af1a-…/payment-schedule/lines/0193af2b-…/convert-to-invoice \ -H 'X-API-Key: sk_live_…' ``` Returns `201` with the created Invoice. The line becomes read-only. Error cases: | HTTP | Code | Cause | |------|-----------------------------------|------------------------------------------------| | 422 | `SCHEDULE_LINE_ALREADY_INVOICED` | The line was already converted. | | 422 | `SCHEDULE_SUM_EXCEEDS_TOTAL` | The deposit total would exceed the quote total HT. | | 409 | `QUOTE_NOT_EDITABLE` | Quote is in a non-accepted status. | #### `GET /api/v1/quotes/{id}/payment-summary` Returns an aggregated view of all schedule lines, already-issued invoices, and SUPER PDP transmission status. ```bash curl https://api.scell.io/api/v1/quotes/0193af1a-…/payment-summary \ -H 'X-API-Key: sk_live_…' ``` Response: ```json { "data": { "schedule": [ { "id": "0193af2b-…", "label": "Acompte 30%", "percent": 30, "amount": 4500.00, "due_date": "2026-06-01", "invoice_id": "0193af9d-…", "invoiced_at": "2026-05-21T08:00:00+00:00" } ], "invoiced": { "total_ht": 4500.00, "count": 1 }, "next_due": { "line_id": "0193af3c-…", "due_date": "2026-09-01", "amount": 10500.00 }, "overdue": { "count": 0, "total_ht": 0.00 }, "superpdp_status": { "submitted": 1, "accepted": 0, "rejected": 0 } } } ``` ### 5.20 Credit packs — `/v1/packs/public`, `/v1/tenant/billing/packs/{slug}/checkout` Credit packs let tenants pre-purchase Scell.io balance in bulk with a bonus tier. The catalogue is **public** (read-only); the checkout triggers a Stripe payment intent (production) or a direct balance credit (sandbox — no real payment). Packs are identified by their `slug`. > There is **no** tenant-facing `/v1/credit-packs` route, and **no** > `/{id}/purchase` route. Listing is at `GET /v1/packs/public`; buying is at > `POST /v1/tenant/billing/packs/{slug}/checkout`. CRUD lives under > `/v1/admin/credit-packs` (admin gate). #### `GET /api/v1/packs/public` Public list of active packs with their bonus tiers. No auth. Cached 1 hour. ```bash curl https://api.scell.io/api/v1/packs/public ``` Response: ```json { "data": [ { "id": "0193af5a-…", "slug": "starter", "name": "Starter", "description": "Pack de démarrage", "amount_eur": 25.00, "amount_euros": 25.00, "credits_eur": 27.50, "credits_euros": 27.50, "bonus_eur": 2.50, "bonus_euros": 2.50, "bonus_percent": 10.0, "currency": "EUR", "position": 1, "is_recommended": false } ] } ``` #### `POST /api/v1/tenant/billing/packs/{slug}/checkout` Buys a pack by its `slug`. In **production** (`sk_live_*`), returns a Stripe `client_secret` to complete payment client-side. In **sandbox** (`sk_test_*`), the balance is credited immediately — no Stripe call. Accepted via Sanctum cookie **or** `X-API-Key`. ```bash # Production — returns a Stripe PaymentIntent curl -X POST https://api.scell.io/api/v1/tenant/billing/packs/starter/checkout \ -H 'X-API-Key: sk_live_…' ``` An unknown or inactive slug returns `404 { "error": { "code": "PACK_NOT_FOUND" } }`. After the Stripe payment succeeds, the Scell.io webhook handler (`payment_intent.succeeded` with `type: "pack_purchase"`) automatically: 1. Credits the tenant's balance by the pack's credit amount. 2. Generates a Factur-X invoice on the master Scell.io company (not the tenant's company). 3. Submits the invoice to SUPER PDP. 4. Emails the PDF invoice to the tenant (`TenantPackInvoiceMail`). ```bash # Sandbox — balance credited immediately, no Stripe interaction curl -X POST https://api.scell.io/api/v1/tenant/billing/packs/starter/checkout \ -H 'X-API-Key: sk_test_…' ``` #### Admin credit pack management (Sanctum + admin gate) | Method | Path | Description | |--------|-----------------------------------|------------------------------------------------| | GET | `/v1/admin/credit-packs` | List all packs (including inactive). | | POST | `/v1/admin/credit-packs` | Create a new pack (auto-creates Stripe Product + Price). | | PUT | `/v1/admin/credit-packs/{id}` | Update name / description / is_active. Syncs Stripe. | | DELETE | `/v1/admin/credit-packs/{id}` | Soft delete + archive Stripe Product. | Admin create/update body: ```bash curl -X POST https://api.scell.io/api/v1/admin/credit-packs \ -H 'Cookie: scellio-session=…' \ -H 'Content-Type: application/json' \ -d '{ "name": "Enterprise 500", "description": "500 crédits — tarif préférentiel grands comptes", "credits": 500, "price_eur": 175.00 }' ``` The endpoint auto-syncs the pack to Stripe (creates a Product + Price) and returns the created pack with `stripe_product_id` and `stripe_price_id`. --- ## 6. End-to-end walkthroughs ### 6.1 B2B invoice with shipping address (Factur-X EN16931) ```bash SCELL=sk_live_a2c4e6... TENANT=acme # 1. Look up the buyer's SIRET (saves typing addresses) curl https://api.scell.io/api/v1/sirene/98765432109876 \ -H "X-API-Key: $SCELL" # 2. Register the buyer once curl -X POST https://api.scell.io/api/v1/buyers \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d '{ "name": "Globex SAS", "country": "FR", "siret": "98765432109876", "vat_number": "FR98765432109", "email": "compta@globex.fr", "billing_address": { "line1": "10 Place Vendome", "postal_code": "75001", "city": "Paris", "country": "FR" }, "shipping_address": { "name": "Globex Entrepot Saint-Ouen", "line1": "42 Rue des Rosiers", "postal_code": "93400", "city": "Saint-Ouen", "country": "FR" } }' # => { "data": { "id": "0193af1a-…", … } } BUYER=0193af1a-... # 3. Issue the invoice using the registry curl -X POST https://api.scell.io/api/v1/invoices \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d "{ \"invoice_date\": \"2026-05-06\", \"due_date\": \"2026-06-05\", \"direction\": \"outgoing\", \"output_format\": \"facturx\", \"currency\": \"EUR\", \"seller_name\": \"Acme SAS\", \"seller_country\": \"FR\", \"seller_siret\": \"12345678901234\", \"seller_vat_number\": \"FR12345678901\", \"seller_address\": { \"line1\": \"1 Rue de la Paix\", \"postal_code\": \"75001\", \"city\": \"Paris\", \"country\": \"FR\" }, \"buyer_id\": \"$BUYER\", \"total_ht\": 1000.00, \"total_tva\": 200.00, \"total_ttc\": 1200.00, \"lines\": [ { \"description\": \"Conseil — mai 2026\", \"quantity\": 5, \"unit\": \"DAY\", \"unit_price_ht\": 200.00, \"tva_rate\": 20, \"total_ht\": 1000.00, \"total_ttc\": 1200.00 } ], \"payment_terms\": \"Paiement a 30 jours fin de mois.\" }" # => { "data": { "id": "INV-…", "status": "draft", … } } INVOICE=INV-... # 4. Submit the invoice curl -X POST "https://api.scell.io/api/v1/invoices/$INVOICE/submit" \ -H "X-API-Key: $SCELL" # 5. Download the Factur-X PDF curl "https://api.scell.io/api/v1/invoices/$INVOICE/download?format=facturx" \ -H "X-API-Key: $SCELL" \ -o invoice-facturx.pdf ``` The generated PDF is Factur-X EN16931 with embedded XML containing BG-12 (BUYER POSTAL ADDRESS), BG-13 (SHIP TO with BT-74 NAME = "Globex Entrepot Saint-Ouen"), BT-84 (IBAN), BT-86 (BIC), BT-20 (payment terms). ### 6.2 B2C particulier invoice ```bash curl -X POST https://api.scell.io/api/v1/invoices \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d '{ "invoice_date": "2026-05-06", "due_date": "2026-05-06", "direction": "outgoing", "output_format": "facturx", "currency": "EUR", "seller_name": "Acme SAS", "seller_country": "FR", "seller_siret": "12345678901234", "seller_address": { "line1": "1 Rue de la Paix", "postal_code": "75001", "city": "Paris", "country": "FR" }, "buyer_name": "Marie Dupont", "buyer_country": "FR", "buyer_is_individual": true, "buyer_address": { "line1": "8 Rue Lepic", "postal_code": "75018", "city": "Paris", "country": "FR" }, "total_ht": 50.00, "total_tva": 10.00, "total_ttc": 60.00, "lines": [ { "description": "Service ponctuel", "quantity": 1, "unit_price_ht": 50.00, "tva_rate": 20, "total_ht": 50.00, "total_ttc": 60.00 } ] }' ``` The Factur-X XML emitted does NOT include BT-46/47/48 (buyer SIRET / VAT / legal id) and skips the L441-10 mention (penalites de retard B2B), in compliance with BR-CO-26 for B2C transactions. ### 6.3 Register a buyer once → reuse across many invoices ```bash # Step 1 — create buyer BUYER_ID=$( curl -X POST https://api.scell.io/api/v1/buyers \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d '{ "name": "Globex SAS", "country": "FR", "siret": "98765432109876", "billing_address": { "line1": "10 Place Vendome", "postal_code": "75001", "city": "Paris", "country": "FR" } }' | jq -r '.data.id' ) echo "Buyer: $BUYER_ID" # Step 2 — issue 12 monthly invoices, all referencing $BUYER_ID for month in 01 02 03 04 05 06 07 08 09 10 11 12; do curl -X POST https://api.scell.io/api/v1/invoices \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d "{ \"invoice_date\": \"2026-${month}-01\", \"due_date\": \"2026-${month}-30\", \"direction\": \"outgoing\", \"output_format\": \"facturx\", \"currency\": \"EUR\", \"seller_name\": \"Acme SAS\", \"seller_country\": \"FR\", \"seller_siret\": \"12345678901234\", \"seller_address\": { \"line1\": \"1 Rue de la Paix\", \"postal_code\": \"75001\", \"city\": \"Paris\", \"country\": \"FR\" }, \"buyer_id\": \"$BUYER_ID\", \"total_ht\": 500, \"total_tva\": 100, \"total_ttc\": 600, \"lines\": [{ \"description\": \"Abonnement mensuel\", \"quantity\": 1, \"unit_price_ht\": 500, \"tva_rate\": 20, \"total_ht\": 500, \"total_ttc\": 600 }] }" done # Step 3 — later, the buyer moves: update the registry only. # Historical invoices keep their snapshot. curl -X PATCH "https://api.scell.io/api/v1/buyers/$BUYER_ID" \ -H "X-API-Key: $SCELL" \ -H 'Content-Type: application/json' \ -d '{ "billing_address": { "line1": "55 Avenue Foch", "postal_code": "75116", "city": "Paris", "country": "FR" } }' # Future invoices snapshot the new address; past ones stay at "10 Place Vendome". ``` ### 6.4 Sub-tenant SuperPDP onboarding (widget v2) End-to-end from the partner platform's perspective: ```bash # 1. Partner backend (server-side, sk_live_*) creates an onboarding # session and returns the SuperPDP authorize URL to the browser. curl -X POST https://api.scell.io/api/v1/onboarding/sessions \ -H 'X-Publishable-Key: pk_live_…' \ -H 'Content-Type: application/json' \ -d '{ "external_id": "merchant-42", "redirect_url": "https://app.merchant-42.fr/onboarding/done", "prefill": { "name": "Merchant 42", "email": "ops@merchant-42.fr" } }' # => { "session_id": "...", "authorize_url": "https://app.superpdp.tech/oauth/authorize?…" } # 2. The browser is redirected to the authorize_url. # SuperPDP collects KYB and identity, then redirects back to: # https://api.scell.io/api/v1/widget/oauth-callback?code=…&state=… # The endpoint exchanges the code, encrypts the SuperPDP tokens, # creates the SubTenant, and posts the result to the parent window # via postMessage. # 3. The widget's parent window receives: window.addEventListener('message', (event) => { if (event.origin !== 'https://api.scell.io') return; if (event.data?.type === 'scell:onboarding:success') { const { sub_tenant_id, secret_key, publishable_key } = event.data; // Pass them to your backend to persist. } }); # 4. Partner backend can now issue invoices on behalf of the sub-tenant: curl -X POST "https://api.scell.io/api/v1/tenant/sub-tenants/$SUB_TENANT_ID/invoices" \ -H "X-Tenant-Key: sk_live_…" \ -H 'Content-Type: application/json' \ -d '{ … }' ``` --- ## 7. Webhooks Webhooks are HTTP POSTs to a URL configured per tenant. Payloads are signed with HMAC-SHA256. ### 7.1 Headers on every delivery | Header | Description | |-------------------------|----------------------------------------------------------| | `Content-Type` | `application/json` | | `X-Scell-Event` | Event name, e.g. `invoice.transmitted`. | | `X-Scell-Delivery-Id` | Unique delivery id (idempotency). | | `X-Scell-Timestamp` | Unix seconds — reject if more than 5 minutes off. | | `X-Scell-Signature` | `sha256=`. See §7.4. | | `X-Scell-Environment` | `production` or `sandbox`. | ### 7.2 Payload shape ```json { "id": "evt_0193af1a...", "type": "invoice.transmitted", "created_at": "2026-05-06T08:42:31+00:00", "tenant_id": "01...", "sub_tenant_id": null, "environment": "production", "data": { "invoice": { "id": "INV-…", "status": "transmitted", … } } } ``` ### 7.3 Event catalogue > Authoritative subscribable list = `App\Models\Webhook::EVENTS`. Outgoing invoices: - `invoice.created` - `invoice.validated` - `invoice.transmitted` - `invoice.accepted` - `invoice.rejected` - `invoice.error` Incoming invoices: - `invoice.incoming.received` - `invoice.incoming.validated` - `invoice.incoming.accepted` - `invoice.incoming.rejected` - `invoice.incoming.disputed` - `invoice.incoming.paid` Signatures: - `signature.created` - `signature.waiting` - `signature.signed` - `signature.completed` - `signature.refused` - `signature.expired` - `signature.error` Account: - `balance.low` - `balance.critical` B2B Onboarding: - `onboarding.started` - `onboarding.step_completed` - `onboarding.completed` - `onboarding.failed` ### 7.4 HMAC verification Signature is the HMAC-SHA256 of the **raw request body** using the webhook's `secret`. Pseudo-code: ```python import hmac, hashlib def verify(secret: str, body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header) ``` ```php function verifyScellSignature(string $secret, string $rawBody, string $header): bool { $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret); return hash_equals($expected, $header); } ``` ```typescript import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(secret: string, rawBody: Buffer, header: string): boolean { const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected, 'utf8'); const b = Buffer.from(header, 'utf8'); return a.length === b.length && timingSafeEqual(a, b); } ``` Always: 1. Compute HMAC over the **raw body** (not parsed JSON). 2. Use a constant-time comparison. 3. Reject deliveries older than 5 minutes (`X-Scell-Timestamp`) to block replay attacks. 4. Treat `X-Scell-Delivery-Id` as an idempotency key — store and dedup. ### 7.5 Retries Failed deliveries (HTTP status not in 2xx) are retried with exponential backoff: | Attempt | Delay (approx) | |---------|----------------| | 1 | immediate | | 2 | 30s | | 3 | 2 min | | 4 | 10 min | | 5 | 1h | | 6 | 6h | | 7 | 24h | After 7 failed attempts the webhook is marked `failed` for that delivery and recorded in `/v1/webhooks/{id}/logs`. The webhook itself is **not** auto-disabled — operate the dashboard or `PUT /v1/webhooks/{id}` {`is_active: false`} to stop deliveries. ### 7.6 Test deliveries ```bash curl -X POST https://api.scell.io/api/v1/webhooks/0193af1a-…/test \ -H 'Authorization: Bearer 1|abc...' ``` Sends a synthetic `webhook.test` event with a stable signature you can replay locally for development. --- ## 8. Errors All error responses are JSON, with a stable shape: ```json { "message": "Human-readable summary", "errors": { "field": ["…"] }, "code": "validation_failed" } ``` `errors` is present only on `422`. `code` is a machine-readable string in `snake_case` for programmatic handling (matches the SDK's exception classes). ### 8.1 Status codes | HTTP | Meaning | When | |------|--------------------------------------|---------------------------------------------------------------------| | 200 | OK | GET, PUT, PATCH, POST when no resource is created. | | 201 | Created | POST creating a new resource. | | 204 | No Content | DELETE, idempotent operations. | | 400 | Bad Request | Malformed body, unknown route parameter. | | 401 | Unauthenticated | Missing / invalid auth credential. | | 403 | Forbidden | Auth ok but action denied (policy, scope, IDOR, ISCA write block). | | 404 | Not Found | Resource missing OR outside scope (anti-IDOR). | | 409 | Conflict | Race-conditioned mutations (e.g. credit note total > invoice total).| | 422 | Unprocessable Entity | Validation failure (`StoreInvoiceRequest`, etc.). | | 429 | Too Many Requests | Rate limit exceeded (per-IP, per-tenant, per-key). | | 451 | Unavailable for Legal Reasons | Fiscal kill-switch active for tenant. | | 500 | Internal Server Error | Unexpected. Retried automatically by SDKs. | | 502 | Bad Gateway | SuperPDP / upstream provider failure. | | 503 | Service Unavailable | Maintenance window. | ### 8.2 Common error bodies #### 401 Unauthenticated ```json { "message": "Unauthenticated.", "code": "unauthenticated" } ``` #### 403 Forbidden ```json { "message": "Forbidden.", "code": "forbidden" } ``` #### 404 Not Found ```json { "message": "Acheteur introuvable.", "code": "not_found" } ``` Note: 404 is also returned when the resource exists but lies outside the caller's `(tenant, sub_tenant)` scope. This is intentional — anti-IDOR. #### IssuerResolver errors (issuance endpoints) When an issuance endpoint (invoices, credit notes, signatures, …) resolves the emitting tenant / sub-tenant / company, `IssuerResolver` may return one of the following. KYB/KYC/onboarding checks apply **only in production** (`sk_live_*`); sandbox keys (`sk_test_*`) skip them. | HTTP | `code` | Meaning | |:----:|-------------------------|-------------------------------------------------------------------------| | 401 | `TENANT_NOT_RESOLVED` | No tenant could be resolved from the auth context (impossible normally).| | 404 | `SUB_TENANT_NOT_FOUND` | `sub_tenant_id` supplied but outside the calling tenant's scope (IDOR). | | 422 | `NO_ISSUER_COMPANY` | No emitting company (tenant without `default_company` and no sub-tenant).| | 403 | `KYB_REQUIRED` | Production: tenant master has not completed KYB. | | 403 | `SUB_TENANT_NOT_READY` | Production: sub-tenant onboarding not yet verified (`canIssueInvoices()` false). | | 403 | `KYC_REQUIRED` | Production: emitting company has not completed KYC. | ```json { "message": "…", "code": "SUB_TENANT_NOT_FOUND" } ``` #### 422 Validation ```json { "message": "The given data was invalid.", "errors": { "buyer_siret": [ "Le SIRET de l'acheteur est requis pour les entreprises francaises (passez buyer_is_individual=true pour un particulier)." ], "lines.0.tva_rate": [ "Le taux de TVA de la ligne 1 doit etre 0%, 2.1%, 5.5%, 10% ou 20%." ] }, "code": "validation_failed" } ``` #### 422 ISCA immutability violation ```json { "message": "Cannot modify fiscal fields on non-draft invoice (ISCA): total_ht, invoice_number", "code": "isca_immutable" } ``` #### 429 Rate limit ```json { "message": "Too Many Attempts.", "code": "rate_limited", "retry_after": 30 } ``` Headers `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After` are also set. #### 451 Fiscal kill-switch ```json { "message": "Fiscal kill-switch is active for this tenant. Write operations are blocked.", "code": "fiscal_kill_switch" } ``` ### 8.3 Rate limits | Surface | Limit | |------------------------------------------|------------------------| | `/v1/auth/login`, `/v1/auth/register` | 5/min/IP | | `/v1/onboarding/exchange` | 5/min/IP (strict) | | `/v1/onboarding/*` (default) | 60/min/IP | | `/v1/tenant/*` | 600/min/key | | `/v1/invoices`, `/v1/credit-notes` | 600/min/key | | `/v1/signatures` (POST) | 120/min/key | | `/mcp` | 600/min/key | Limits are enforced at the middleware layer (`tenant.rate-limit`, `api.rate-limit`, `onboarding.rate-limit`). On 429, back off using exponential jitter; SDKs do this automatically. --- ## 9. Compliance ### 9.1 ISCA fiscal autocertification (PAS NF525) Scell.io is **autocertified** under the French CNIL / DGFiP framework. The platform is **not** NF525-certified (no third-party certification body). The technical concept used is **ISCA**: Integrity, Securisation, Conservation, Archivage — implemented as an immutable hash-chained ledger. References: - Architecture: `app/Services/Fiscal/FiscalLedgerService.php` - Triggers: PostgreSQL `AFTER INSERT/UPDATE` on `invoices` & `credit_notes` (defense in depth). - Per-pair (tenant, sub_tenant) chains: a tampering on one sub_tenant's chain does not corrupt the parent or other sub_tenants. - Daily closure command: `php artisan fiscal:daily-closing` (00:05). - Backfill safety net: `php artisan fiscal:backfill-entries` (daily). - Health check: `GET /v1/tenant/fiscal/integrity` + CLI `php artisan fiscal:integrity-check`. - OpenTimestamps Bitcoin anchoring (best effort, opt-in via `FISCAL_OTS_ENABLED=true`). - TSA RFC 3161 anchoring (opt-in via `FISCAL_AUTO_ANCHOR=true`). Immutability rules: - `Invoice::IMMUTABLE_FISCAL_FIELDS` and `CreditNote::IMMUTABLE_FISCAL_FIELDS` (totals, numbers, parties, currency, dates) are blocked on mutation once status leaves `draft`. - A non-draft credit note **cannot be deleted**: the model boot guard raises `RuntimeException`, returned as `422` with code `isca_immutable`. ### 9.2 Snapshot pattern on invoices When an invoice is created with a `buyer_id` (registry shortcut), the controller copies the registry's current state into the denormalised `buyer_*` columns and the `buyer_address` / `buyer_shipping_address` JSON columns. **These columns are the source of truth** for fiscal artifacts (Factur-X PDF, UBL, CII, FEC export). Mutating the registry buyer afterwards never alters historical invoices. ### 9.3 Factur-X profiles | Profile | Use case | |-------------|---------------------------------------------------------------| | `MINIMUM` | Internal control only. Not a legal e-invoice. | | `BASIC_WL` | Basic without lines. | | `BASIC` | Basic with lines. EN16931 subset. | | `EN_16931` | **Default Scell.io**. Full EN16931 conformance, B2B & B2C. | | `EXTENDED` | French extensions (Chorus Pro). On request. | Generation goes through `App\Services\HorstekoFacturXGenerator`. ISCA Schematron validation is run in CI; submissions failing Schematron get status `rejected` with the rule reference. ### 9.4 Notable BR rules enforced | Rule | Enforcement | |-------------|-----------------------------------------------------------------------| | `BR-CO-26` | B2C: BT-46/BT-47/BT-48 (buyer SIRET/VAT/legal id) omitted. | | `BR-CO-27` | When VAT rate > 0, seller VAT identifier required (BT-31 or BT-32). | | `BR-FR-05` | L441-10 mention (penalites de retard B2B + 40 EUR fixed indemnity) emitted only for B2B. Stripped for B2C.| | `BR-CO-15` | Sum of line totals = `total_ht`. | | `BR-CO-16` | `total_tva` = sum of VAT category amounts. | | `BR-CO-17` | `total_ttc` = `total_ht + total_tva`. | ### 9.5 Daily closure Cron 00:05 every day. Per tenant + per active sub_tenant: 1. Builds the closure CSV: `increment_id, type, saving_date, document_date, total_excl_tax` + `Total` line. 2. Stores on S3. 3. Emits `closing_hash` and writes the FiscalClosing. 4. Sends a recap email (`DailyClosureMail`) to `tenant.email` (or `subTenant.contact_email` for sub-tenants) with a 5-day signed URL to the CSV. 5. Best-effort OpenTimestamps submission. The recap email is sent even when 0 transactions occurred (audit trail). ### 9.6 Right to be forgotten (RGPD) Personal data fields (`buyer_email`, `buyer_phone`, signer phone) can be purged on request via the dashboard's RGPD flow. Fiscal-locked fields (`buyer_name`, `buyer_address`, identity ids) **cannot** be purged because the invoice ledger is immutable — the obligation to keep them 6 to 10 years (CGI art. L102 B) overrides the RGPD erasure right (CNIL guidance 2018). --- ## 10. Versioning & change policy - The API is versioned via the `/v1` URL prefix. A `/v2` will be introduced for breaking changes; both versions will run in parallel for at least 12 months. - Within `v1`, additive changes (new endpoints, new optional fields, new enum values) are made without notice. - Breaking changes inside `v1` are reserved for security or fiscal compliance fixes and are announced on the changelog with at least 2 weeks of advance notice. - Field deprecations are marked in the OpenAPI spec (`storage/api-docs/api-docs.json`) and removed at the next major. - The legacy `/v1/tenant/*` family is frozen — no new endpoints, only bug fixes. New integrations should use the unified `/v1/*` surface with `X-API-Key`. - The `Company.logo_url` field is on a deprecation track (replaced by `InvoiceTemplate`; see CLAUDE.md for the roadmap). --- ## Cheat sheet ```text Auth X-API-Key: sk_(test|live)_… server-to-server X-Publishable-Key: pk_(test|live)_… browser widgets X-Tenant-Key: sk_(test|live)_… legacy tenant routes Cookie + XSRF Sanctum SPA dashboard Hostname https://api.scell.io same for prod and sandbox sk_test_* → routed to rdb_sandbox sk_live_* → routed to rdb (production) Core endpoints POST /api/v1/buyers register a customer POST /api/v1/invoices issue an invoice POST /api/v1/invoices/{id}/submit transmit POST /api/v1/invoices/{id}/mark-paid manual payment POST /api/v1/invoices/{id}/send-by-email email Factur-X PDF to buyer GET /api/v1/invoices/{id}/download PDF or XML POST /api/v1/credit-notes refund (linked to invoice) POST /api/v1/signatures eIDAS EU-SES POST /api/v1/quotes commercial proposal POST /api/v1/quotes/{id}/send email to buyer POST /api/v1/quotes/{id}/convert-to-deposit type 386 invoice POST /api/v1/quotes/{id}/convert-to-balance type 380 invoice GET /api/v1/quotes/{id}/payment-schedule milestones POST /api/v1/quotes/{id}/payment-schedule/lines/{lineId}/convert-to-invoice GET /api/v1/packs/public available packs (public) POST /api/v1/tenant/billing/packs/{slug}/checkout buy a pack Buyer registry - Set buyer_id on invoice → snapshot copied to invoice. - Update buyer later → historical invoices unaffected. - Shipping address (BG-13) lives on Buyer or per invoice override. ISCA compliance - Invoices/credit-notes immutable once non-draft. - Sent credit notes cannot be deleted. - Daily closure at 00:05 with email + S3 CSV + OTS. - Per (tenant, sub_tenant) hash chains. - PAS NF525 — autocertification only. Webhooks - HMAC-SHA256 over raw body, header X-Scell-Signature. - Reject if X-Scell-Timestamp > 5 min skew. - Idempotency on X-Scell-Delivery-Id. - 7 retries with exponential backoff up to 24h. Errors 401 unauth | 403 forbidden | 404 not found / IDOR 422 validation / ISCA | 429 rate-limited | 451 kill-switch Sandboxing sk_test_* → DB rdb_sandbox, no real fiscal effect. Identical endpoints, no host change. ``` ## Signature blocks (v2.12.0 — paraphe, mentions, date) `POST /api/v1/signatures` accepts 3 new optional blocks. Scell pre-processes the PDF server-side via TCPDF/FPDI BEFORE forwarding to certified eSignature service (which does NOT natively support handwritten mentions). The original PDF hash is preserved; a `processed_file_hash` is added for traceability. ### Initials block (v2.15.0 — multi-page positions[]) The initials block has TWO accepted formats: 1. **New (recommended, since v2.15.0)** — `positions[]` with one entry per page. Each entry can override `x`, `y`, `font_size`, `color`, `bold`. 2. **Legacy (still supported)** — `position` (single common position) + `pages` descriptor (`'all'`, `'except_last'`, or array of int). If BOTH formats are provided, `positions[]` wins (server priority). ```jsonc { // NEW format (v2.15.0): positions[] with per-page placement "initials_block": { "enabled": true, "mode": "auto", // 'auto' | 'custom' "source": "signer_name", // 'signer_name' | 'custom' "custom_text": null, // string <= 8 chars (when source=custom) "font_size": 10, // block default (overridable per-position) "color": "#333333", // block default (overridable per-position) "bold": false, // block default (overridable per-position) "positions": [ { "page": 1, "x": 90, "y": 90, "unit": "percent" }, { "page": 2, "x": 88, "y": 92, "unit": "percent", "font_size": 12 }, { "page": 3, "x": 85, "y": 90, "unit": "percent", "color": "#AA0000" } ] } } ``` ```jsonc { // LEGACY format (still works): single position + pages descriptor "initials_block": { "enabled": true, "mode": "auto", "source": "signer_name", "pages": "except_last", // 'all' | 'except_last' | [1, 3, 5] "position": { "x": 90, "y": 95, "unit": "percent" }, "font_size": 10, "color": "#333333" } } ``` ### Mentions & date_block ```jsonc { // Mentions (max 20) — legal mentions burned on the PDF "mentions": [{ "label": "Lu et approuve", "required": true, "signer_index": 0, // 0-based index into signers[] "position": { "page": 1, "x": 10, "y": 80, "w": 60, "h": 8, "unit": "percent" }, "fallback_text": "Lu et approuve", "font_size": 10, "color": "#000000" }], // Date block — today's date in tenant timezone "date_block": { "enabled": true, "format": "d/m/Y", // PHP date() format "timezone": "Europe/Paris", // IANA "position": { "page": "last", "x": 80, "y": 10, "unit": "percent" }, "font_size": 10, "color": "#000000" } } ``` Validation rules: - `font_size` in [6, 20] - `color` hex regex `^#[0-9A-Fa-f]{6}$` - `position.unit` / `positions.*.unit` in ['percent', 'pixel'] - `mentions.*.signer_index` must point to an existing signer (0..signers.length-1) - `initials_block.positions[]` accepts up to 500 entries (one per page) - `initials_block.positions.*.page` integer in [1, 500] (1-indexed) - `initials_block.pages` (legacy) accepts 'all' | 'except_last' | array of int - `date_block.position.page` accepts int (1-indexed) or string 'last' --- End of `scell-api-llms.txt`. For SDK-specific guidance see `scell-sdk-{js,php,mcp-agent}-llms.txt` siblings.