Skip to content

Integrer l'API Scell.io

Ce guide vous accompagne pour integrer l'API Scell.io dans votre application. De la premiere connexion a un systeme complet avec webhooks, vous aurez toutes les cles pour automatiser la facturation electronique et la signature de documents.

Ce tutorial est concu pour les developpeurs. Chaque etape inclut des exemples en curl, JavaScript, PHP et Python.


Ce que vous allez apprendre

  1. Les deux methodes d'authentification et quand les utiliser
  2. Faire votre premier appel API et verifier que tout fonctionne
  3. Creer des factures et des signatures via l'API
  4. Recevoir des notifications en temps reel avec les webhooks
  5. Gerer les erreurs correctement
  6. Utiliser le sandbox pour tester sans frais
  7. Les bonnes pratiques pour une integration solide

Prerequis

  • Un compte Scell.io actif avec une entreprise configuree (SIRET verifie)
  • Une cle API generee depuis le dashboard
  • Un environnement de developpement avec au choix : curl, Node.js, PHP ou Python

Etape 1 : Authentification

L'API Scell.io propose deux methodes d'authentification. Chacune a son usage.

Option A : Token Bearer (pour le dashboard et les applications web)

Le token Bearer est obtenu en se connectant avec email et mot de passe. Il est ideal pour les applications qui agissent au nom d'un utilisateur connecte.

Obtenir un token :

bash
curl -X POST https://api.scell.io/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "votre@email.com",
    "password": "votre_mot_de_passe"
  }'

Reponse :

json
{
  "message": "Connexion reussie",
  "token": "1|abc123def456ghi789...",
  "user": {
    "id": "550e8400-...",
    "name": "Jean Dupont",
    "email": "votre@email.com"
  }
}

Utiliser le token :

bash
curl -X GET https://api.scell.io/api/v1/invoices \
  -H "Authorization: Bearer 1|abc123def456ghi789..."

Option B : Cle API (pour les integrations serveur-a-serveur)

La cle API est la methode recommandee pour les integrations automatisees. Elle est liee a une entreprise et un environnement (sandbox ou production).

Utiliser la cle API :

bash
curl -X GET https://api.scell.io/api/v1/invoices \
  -H "X-API-Key: sk_live_VOTRE_CLE_API"

Quelle methode choisir ?

CritereToken BearerCle API
UsageDashboard, applications webIntegrations back-end, scripts, cron jobs
Duree de vieExpire a la deconnexionPermanent (sauf revocation)
CreationVia login email/mot de passeVia le dashboard (section Cles API)
Header HTTPAuthorization: Bearer {token}X-API-Key: {cle}
Actions possiblesToutes (lecture + ecriture)Lecture + creation factures/signatures

Astuce : Pour une integration en production, utilisez toujours une cle API. Le token Bearer est pratique pour le developpement et le dashboard, mais il n'est pas concu pour les appels automatises.


Etape 2 : Premier appel API

Avant de creer quoi que ce soit, verifions que votre connexion fonctionne en consultant votre solde :

curl

bash
curl -X GET https://api.scell.io/api/v1/balance \
  -H "Authorization: Bearer VOTRE_TOKEN"

JavaScript (fetch)

javascript
const API_URL = 'https://api.scell.io/api/v1';
const API_KEY = process.env.SCELL_API_KEY; // sk_live_... (aussi appele SCELL_TENANT_KEY — c'est la meme cle sk_*)

async function getBalance() {
  const response = await fetch(`${API_URL}/balance`, {
    headers: {
      'X-API-Key': API_KEY,
    },
  });

  if (!response.ok) {
    throw new Error(`Erreur HTTP ${response.status}: ${response.statusText}`);
  }

  const data = await response.json();
  console.log(`Solde actuel : ${data.data.amount} ${data.data.currency}`);
  return data;
}

getBalance();

PHP (Guzzle)

php
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.scell.io/api/v1/',
    'headers' => [
        'X-API-Key' => getenv('SCELL_API_KEY'),
        'Accept' => 'application/json',
    ],
]);

try {
    $response = $client->get('balance');
    $data = json_decode($response->getBody(), true);
    echo "Solde actuel : {$data['data']['amount']} {$data['data']['currency']}\n";
} catch (\GuzzleHttp\Exception\ClientException $e) {
    echo "Erreur : " . $e->getResponse()->getBody() . "\n";
}

Python (requests)

python
import os
import requests

API_URL = 'https://api.scell.io/api/v1'
API_KEY = os.environ['SCELL_API_KEY']

response = requests.get(
    f'{API_URL}/balance',
    headers={'X-API-Key': API_KEY}
)

if response.ok:
    data = response.json()
    print(f"Solde actuel : {data['data']['amount']} {data['data']['currency']}")
else:
    print(f"Erreur {response.status_code}: {response.text}")

Reponse attendue :

json
{
  "data": {
    "amount": 150.00,
    "currency": "EUR",
    "auto_reload_enabled": false,
    "auto_reload_threshold": null,
    "auto_reload_amount": null,
    "low_balance_alert_threshold": 10.00,
    "critical_balance_alert_threshold": 2.00
  }
}

Si vous voyez votre solde, tout est en ordre. Passons a la creation de factures.


Etape 3 : Creer une facture

Voici un exemple complet de creation de facture avec tous les champs documentes.

curl

bash
curl -X POST https://api.scell.io/api/v1/invoices \
  -H "X-API-Key: sk_live_VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "FACT-2026-001",
    "direction": "outgoing",
    "output_format": "facturx",
    "issue_date": "2026-03-03",
    "due_date": "2026-04-02",
    "currency": "EUR",
    "total_ht": 1500.00,
    "total_tax": 300.00,
    "total_ttc": 1800.00,
    "seller_siret": "12345678901234",
    "seller_name": "Ma Societe SAS",
    "seller_address": {
      "line1": "10 rue de la Paix",
      "postal_code": "75002",
      "city": "Paris",
      "country": "FR"
    },
    "buyer_siret": "98765432109876",
    "buyer_name": "Client SARL",
    "buyer_address": {
      "line1": "25 avenue des Champs-Elysees",
      "postal_code": "75008",
      "city": "Paris",
      "country": "FR"
    },
    "lines": [
      {
        "description": "Developpement site web - Phase 1",
        "quantity": 5,
        "unit_price": 300.00,
        "tax_rate": 20.00,
        "total_ht": 1500.00,
        "total_tax": 300.00,
        "total_ttc": 1800.00
      }
    ]
  }'

JavaScript (fetch)

javascript
async function createInvoice() {
  const response = await fetch(`${API_URL}/invoices`, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      invoice_number: 'FACT-2026-001',
      direction: 'outgoing',
      output_format: 'facturx',
      issue_date: '2026-03-03',
      due_date: '2026-04-02',
      currency: 'EUR',
      total_ht: 1500.00,
      total_tax: 300.00,
      total_ttc: 1800.00,
      seller_siret: '12345678901234',
      seller_name: 'Ma Societe SAS',
      seller_address: {
        line1: '10 rue de la Paix',
        postal_code: '75002',
        city: 'Paris',
        country: 'FR',
      },
      buyer_siret: '98765432109876',
      buyer_name: 'Client SARL',
      buyer_address: {
        line1: '25 avenue des Champs-Elysees',
        postal_code: '75008',
        city: 'Paris',
        country: 'FR',
      },
      lines: [
        {
          description: 'Developpement site web - Phase 1',
          quantity: 5,
          unit_price: 300.00,
          tax_rate: 20.00,
          total_ht: 1500.00,
          total_tax: 300.00,
          total_ttc: 1800.00,
        },
      ],
    }),
  });

  if (response.status === 201) {
    const data = await response.json();
    console.log('Facture creee :', data.data.id);
    return data;
  } else {
    const error = await response.json();
    console.error('Erreur :', error);
    throw new Error(error.message);
  }
}

PHP (Guzzle)

php
<?php
try {
    $response = $client->post('invoices', [
        'json' => [
            'invoice_number' => 'FACT-2026-001',
            'direction' => 'outgoing',
            'output_format' => 'facturx',
            'issue_date' => '2026-03-03',
            'due_date' => '2026-04-02',
            'currency' => 'EUR',
            'total_ht' => 1500.00,
            'total_tax' => 300.00,
            'total_ttc' => 1800.00,
            'seller_siret' => '12345678901234',
            'seller_name' => 'Ma Societe SAS',
            'seller_address' => [
                'line1' => '10 rue de la Paix',
                'postal_code' => '75002',
                'city' => 'Paris',
                'country' => 'FR',
            ],
            'buyer_siret' => '98765432109876',
            'buyer_name' => 'Client SARL',
            'buyer_address' => [
                'line1' => '25 avenue des Champs-Elysees',
                'postal_code' => '75008',
                'city' => 'Paris',
                'country' => 'FR',
            ],
            'lines' => [
                [
                    'description' => 'Developpement site web - Phase 1',
                    'quantity' => 5,
                    'unit_price' => 300.00,
                    'tax_rate' => 20.00,
                    'total_ht' => 1500.00,
                    'total_tax' => 300.00,
                    'total_ttc' => 1800.00,
                ],
            ],
        ],
    ]);

    $data = json_decode($response->getBody(), true);
    echo "Facture creee : {$data['data']['id']}\n";
} catch (\GuzzleHttp\Exception\ClientException $e) {
    $error = json_decode($e->getResponse()->getBody(), true);
    echo "Erreur de validation :\n";
    foreach ($error['errors'] ?? [] as $field => $messages) {
        echo "  - {$field}: " . implode(', ', $messages) . "\n";
    }
}

Python (requests)

python
invoice_data = {
    'invoice_number': 'FACT-2026-001',
    'direction': 'outgoing',
    'output_format': 'facturx',
    'issue_date': '2026-03-03',
    'due_date': '2026-04-02',
    'currency': 'EUR',
    'total_ht': 1500.00,
    'total_tax': 300.00,
    'total_ttc': 1800.00,
    'seller_siret': '12345678901234',
    'seller_name': 'Ma Societe SAS',
    'seller_address': {
        'line1': '10 rue de la Paix',
        'postal_code': '75002',
        'city': 'Paris',
        'country': 'FR',
    },
    'buyer_siret': '98765432109876',
    'buyer_name': 'Client SARL',
    'buyer_address': {
        'line1': '25 avenue des Champs-Elysees',
        'postal_code': '75008',
        'city': 'Paris',
        'country': 'FR',
    },
    'lines': [
        {
            'description': 'Developpement site web - Phase 1',
            'quantity': 5,
            'unit_price': 300.00,
            'tax_rate': 20.00,
            'total_ht': 1500.00,
            'total_tax': 300.00,
            'total_ttc': 1800.00,
        },
    ],
}

response = requests.post(
    f'{API_URL}/invoices',
    headers={
        'X-API-Key': API_KEY,
        'Content-Type': 'application/json',
    },
    json=invoice_data,
)

if response.status_code == 201:
    data = response.json()
    print(f"Facture creee : {data['data']['id']}")
else:
    print(f"Erreur {response.status_code}:")
    errors = response.json().get('errors', {})
    for field, messages in errors.items():
        print(f"  - {field}: {', '.join(messages)}")

Description des champs

ChampTypeObligatoireDescription
invoice_numberstringOuiVotre numero de facture interne
directionstringOuioutgoing (vente) ou incoming (achat)
output_formatstringOuifacturx, ubl ou cii
issue_datedateOuiDate d'emission (format YYYY-MM-DD)
due_datedateNonDate d'echeance
currencystringNonDevise ISO 4217 (defaut: EUR)
total_htnumberOuiMontant total hors taxes
total_taxnumberOuiMontant total de la TVA
total_ttcnumberOuiMontant total TTC
seller_siretstringOuiSIRET du vendeur (14 chiffres)
seller_namestringOuiRaison sociale du vendeur
seller_addressobjectOuiAdresse complete du vendeur
buyer_siretstringOuiSIRET de l'acheteur (14 chiffres)
buyer_namestringOuiRaison sociale de l'acheteur
buyer_addressobjectOuiAdresse complete de l'acheteur
linesarrayOuiLignes de facture (au moins 1)
external_idstringNonVotre identifiant interne pour retrouver la facture
archive_enabledbooleanNonActiver l'archivage longue duree

Chaque ligne de facture contient :

ChampTypeObligatoireDescription
descriptionstringOuiDescription de la prestation ou du produit
quantitynumberOuiQuantite
unit_pricenumberOuiPrix unitaire HT
tax_ratenumberOuiTaux de TVA en pourcentage (ex: 20.00)
total_htnumberOuiTotal HT de la ligne
total_taxnumberOuiTotal TVA de la ligne
total_ttcnumberOuiTotal TTC de la ligne

Etape 4 : Configurer les Webhooks

Les webhooks vous permettent de recevoir des notifications en temps reel quand un evenement se produit sur vos factures ou signatures. Au lieu d'interroger l'API toutes les minutes pour verifier les statuts, c'est l'API qui vous previent.

Pourquoi utiliser les webhooks ?

Sans webhook, vous devriez faire du "polling" :

Votre serveur                     Scell.io
     |                                |
     |--- GET /invoices/123 --------->|  (rien de nouveau)
     |<--- status: transmitted -------|
     |                                |
     |  ... 1 minute plus tard ...    |
     |--- GET /invoices/123 --------->|  (toujours rien)
     |<--- status: transmitted -------|
     |                                |
     |  ... 1 minute plus tard ...    |
     |--- GET /invoices/123 --------->|  (changement !)
     |<--- status: completed ---------|

Avec un webhook, Scell.io vous previent immediatement :

Votre serveur                     Scell.io
     |                                |
     |                                |  (statut change)
     |<--- POST /votre-webhook -------|  event: invoice.validated
     |--- 200 OK ------------------->|

Creer un webhook

bash
curl -X POST https://api.scell.io/api/v1/webhooks \
  -H "Authorization: Bearer VOTRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://mon-site.com/webhooks/scell",
    "events": [
      "invoice.validated",
      "invoice.rejected",
      "signature.completed",
      "signature.refused"
    ],
    "environment": "production"
  }'

Reponse :

json
{
  "message": "Webhook cree avec succes",
  "data": {
    "id": "wh_abc123...",
    "url": "https://mon-site.com/webhooks/scell",
    "events": ["invoice.validated", "invoice.rejected", "signature.completed", "signature.refused"],
    "is_active": true,
    "environment": "production",
    "secret": "whsec_abc123def456ghi789..."
  }
}

Attention : Conservez le secret en lieu sur. Il ne sera plus affiche. Vous en aurez besoin pour verifier l'authenticite des webhooks recus.

Verifier la signature des webhooks

Chaque webhook envoye par Scell.io est signe avec votre secret. Verifiez toujours cette signature pour vous assurer que la requete vient bien de Scell.io et n'a pas ete alteree.

Le header X-Scell-Signature contient le timestamp et la signature HMAC :

X-Scell-Signature: t=1709467800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Exemple de handler en Node.js (Express)

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

const WEBHOOK_SECRET = process.env.SCELL_WEBHOOK_SECRET;

// Middleware pour parser le body brut (necessaire pour la verification de signature)
app.post('/webhooks/scell', express.raw({ type: 'application/json' }), (req, res) => {
  // 1. Extraire le timestamp et la signature du header
  const signatureHeader = req.headers['x-scell-signature'];
  if (!signatureHeader) {
    return res.status(401).send('Signature manquante');
  }

  const parts = Object.fromEntries(
    signatureHeader.split(',').map(part => part.split('='))
  );
  const timestamp = parts.t;
  const receivedSignature = parts.v1;

  // 2. Verifier que le timestamp n'est pas trop ancien (protection replay)
  const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 300;
  if (parseInt(timestamp) < fiveMinutesAgo) {
    return res.status(401).send('Timestamp trop ancien');
  }

  // 3. Calculer la signature attendue
  const payload = `${timestamp}.${req.body.toString()}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  // 4. Comparer les signatures
  if (!crypto.timingSafeEqual(
    Buffer.from(receivedSignature),
    Buffer.from(expectedSignature)
  )) {
    return res.status(401).send('Signature invalide');
  }

  // 5. Traiter l'evenement
  const event = JSON.parse(req.body.toString());
  const eventType = req.headers['x-scell-event'];

  switch (eventType) {
    case 'invoice.validated':
      console.log(`Facture ${event.data.id} validee`);
      // Mettre a jour votre base de donnees...
      break;

    case 'signature.completed':
      console.log(`Signature ${event.data.id} terminee`);
      // Telecharger le document signe...
      break;

    case 'invoice.rejected':
      console.log(`Facture ${event.data.id} rejetee : ${event.data.status_message}`);
      // Alerter l'utilisateur...
      break;

    default:
      console.log(`Evenement non gere : ${eventType}`);
  }

  // 6. Repondre 200 rapidement
  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Webhook listener demarre sur le port 3000'));

Exemple de handler en PHP (Laravel)

php
<?php
// routes/web.php
Route::post('/webhooks/scell', [WebhookReceiverController::class, 'handle']);

// app/Http/Controllers/WebhookReceiverController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class WebhookReceiverController extends Controller
{
    public function handle(Request $request)
    {
        // 1. Extraire la signature
        $signatureHeader = $request->header('X-Scell-Signature');
        if (!$signatureHeader) {
            return response('Signature manquante', 401);
        }

        // 2. Parser le timestamp et la signature
        $parts = [];
        foreach (explode(',', $signatureHeader) as $part) {
            [$key, $value] = explode('=', $part, 2);
            $parts[$key] = $value;
        }

        $timestamp = $parts['t'] ?? null;
        $receivedSignature = $parts['v1'] ?? null;

        // 3. Verifier le timestamp (5 minutes max)
        if (abs(time() - (int) $timestamp) > 300) {
            return response('Timestamp trop ancien', 401);
        }

        // 4. Verifier la signature
        $payload = $timestamp . '.' . $request->getContent();
        $expectedSignature = hash_hmac('sha256', $payload, config('services.scell.webhook_secret'));

        if (!hash_equals($expectedSignature, $receivedSignature)) {
            return response('Signature invalide', 401);
        }

        // 5. Traiter l'evenement
        $eventType = $request->header('X-Scell-Event');
        $data = $request->all();

        match ($eventType) {
            'invoice.validated' => $this->handleInvoiceValidated($data),
            'signature.completed' => $this->handleSignatureCompleted($data),
            'invoice.rejected' => $this->handleInvoiceRejected($data),
            default => Log::info("Evenement webhook non gere : {$eventType}"),
        };

        return response('OK', 200);
    }

    private function handleInvoiceValidated(array $data): void
    {
        Log::info("Facture validee : {$data['data']['id']}");
        // Mettre a jour votre base de donnees...
    }

    private function handleSignatureCompleted(array $data): void
    {
        Log::info("Signature terminee : {$data['data']['id']}");
        // Telecharger le document signe...
    }

    private function handleInvoiceRejected(array $data): void
    {
        Log::warning("Facture rejetee : {$data['data']['id']}");
        // Alerter l'utilisateur...
    }
}

Tester votre webhook

Une fois votre endpoint configure, testez-le depuis le dashboard ou via l'API :

bash
curl -X POST https://api.scell.io/api/v1/webhooks/wh_abc123.../test \
  -H "Authorization: Bearer VOTRE_TOKEN"

Reponse :

json
{
  "success": true,
  "status_code": 200,
  "response_time_ms": 142
}

Consulter les logs de webhook

Si un webhook echoue, consultez les logs pour comprendre pourquoi :

bash
curl -X GET https://api.scell.io/api/v1/webhooks/wh_abc123.../logs \
  -H "Authorization: Bearer VOTRE_TOKEN"

Etape 5 : Gerer les erreurs

Codes HTTP standards

L'API Scell.io utilise les codes HTTP standards :

CodeSignificationAction recommandee
200SuccesTraiter la reponse
201Creation reussieTraiter la nouvelle ressource
400Requete invalideCorriger la requete
401Non authentifieVerifier votre cle API ou token
403Non autoriseVerifier vos permissions
404Ressource non trouveeVerifier l'identifiant
422Erreur de validationCorriger les champs en erreur
429Trop de requetesRalentir et reessayer
500Erreur serveurReessayer apres quelques secondes

Structure des erreurs JSON

Toutes les erreurs suivent le meme format :

json
{
  "message": "The given data was invalid.",
  "errors": {
    "seller_siret": ["Le SIRET du vendeur est requis."],
    "lines": ["Au moins une ligne de facture est requise."]
  }
}

Ou pour les erreurs non liees a la validation :

json
{
  "error": "Solde insuffisant",
  "code": "INSUFFICIENT_BALANCE"
}

Strategie de retry recommandee

Pour les erreurs temporaires (codes 429 et 5xx), utilisez un backoff exponentiel :

javascript
async function callWithRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const statusCode = error.response?.status;

      // Ne pas reessayer les erreurs client (sauf 429)
      if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
        throw error;
      }

      if (attempt === maxRetries) {
        throw error;
      }

      // Backoff exponentiel : 1s, 2s, 4s
      const delay = Math.pow(2, attempt) * 1000;
      console.log(`Tentative ${attempt + 1} echouee, retry dans ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Utilisation
const invoice = await callWithRetry(() => createInvoice(data));
python
import time

def call_with_retry(fn, max_retries=3):
    for attempt in range(max_retries + 1):
        try:
            return fn()
        except requests.exceptions.HTTPError as e:
            status_code = e.response.status_code

            # Ne pas reessayer les erreurs client (sauf 429)
            if 400 <= status_code < 500 and status_code != 429:
                raise

            if attempt == max_retries:
                raise

            delay = (2 ** attempt)
            print(f"Tentative {attempt + 1} echouee, retry dans {delay}s...")
            time.sleep(delay)

Etape 6 : Utiliser le Sandbox

Le sandbox est un environnement de test complet qui fonctionne exactement comme la production, mais sans debiter vos credits.

URLs

EnvironnementURL de base
Productionhttps://api.scell.io/api/v1/
Sandboxhttps://api.scell.io/api/v1/sandbox/

Cle API sandbox

Creez une cle API dediee au sandbox depuis le dashboard :

  1. Allez dans Cles API
  2. Cliquez sur Nouvelle cle
  3. Selectionnez l'environnement sandbox
  4. La cle generee sera de la forme sk_test_...

Donnees de test

En sandbox, utilisez les donnees que vous voulez. Les SIRET ne sont pas verifies contre l'INSEE, les SMS OTP ne sont pas envoyes (le code est toujours 000000), et aucun credit n'est debite.

Basculer en production

Quand vos tests sont concluants :

  1. Creez une cle API production (sk_live_...)
  2. Rechargez votre solde (minimum 10 EUR)
  3. Changez l'URL de base et la cle API dans votre application
  4. Configurez les webhooks pour l'environnement production
javascript
// Configuration par environnement
const config = {
  development: {
    apiUrl: 'https://api.scell.io/api/v1/sandbox',
    apiKey: process.env.SCELL_TEST_API_KEY,
  },
  production: {
    apiUrl: 'https://api.scell.io/api/v1',
    apiKey: process.env.SCELL_LIVE_API_KEY,
  },
};

const env = process.env.NODE_ENV || 'development';
const { apiUrl, apiKey } = config[env];

Attention : Ne melangez jamais les cles sandbox et production. Une cle sandbox ne fonctionnera pas sur les endpoints de production, et vice versa.


Bonnes Pratiques

1. Stocker la cle API en variable d'environnement

Ne codez jamais votre cle API en dur dans le code source :

bash
# .env (ajoute au .gitignore !)
SCELL_API_KEY=sk_live_abc123...        # Aussi appele SCELL_TENANT_KEY — c'est la meme cle (sk_*)
SCELL_WEBHOOK_SECRET=whsec_def456...   # Unique par webhook (genere dans le dashboard)
javascript
// Bien
const apiKey = process.env.SCELL_API_KEY;

// Mal -- ne faites JAMAIS ceci
const apiKey = 'sk_live_abc123...';

2. Utiliser le champ external_id pour l'idempotence

Si votre application peut envoyer la meme requete deux fois (retry apres timeout, par exemple), utilisez le champ external_id pour eviter les doublons :

json
{
  "external_id": "ma-facture-interne-42",
  "invoice_number": "FACT-2026-042",
  ...
}

Vous pouvez ensuite retrouver la facture par son external_id sans risque de creer un doublon.

3. Respecter le rate limiting

L'API impose des limites de requetes pour garantir la disponibilite pour tous. Si vous recevez un code 429 :

  • Attendez le delai indique dans le header Retry-After
  • Implementez un backoff exponentiel (voir la section Gerer les erreurs)
  • Regroupez vos requetes quand c'est possible

4. Paginer les listes

Les endpoints de liste retournent des resultats pagines. Utilisez les parametres per_page et page :

bash
# Page 1, 50 resultats par page
curl "https://api.scell.io/api/v1/invoices?per_page=50&page=1" \
  -H "X-API-Key: sk_live_..."

Reponse avec metadonnees de pagination :

json
{
  "data": [...],
  "meta": {
    "current_page": 1,
    "last_page": 5,
    "per_page": 50,
    "total": 237
  }
}

5. Surveiller votre solde

Configurez les alertes de solde bas pour ne jamais etre bloque :

bash
curl -X PUT https://api.scell.io/api/v1/balance/settings \
  -H "Authorization: Bearer VOTRE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_reload_enabled": true,
    "auto_reload_threshold": 10.00,
    "auto_reload_amount": 100.00,
    "low_balance_alert_threshold": 20.00,
    "critical_balance_alert_threshold": 5.00
  }'

Avec cette configuration :

  • Vous recevez une alerte quand votre solde passe sous 20 EUR
  • Vous recevez une alerte critique quand il passe sous 5 EUR
  • Votre solde est automatiquement recharge de 100 EUR quand il passe sous 10 EUR

Exemples Complets

Application Node.js

Un module complet reutilisable pour integrer Scell.io dans une application Node.js :

javascript
// scell-client.js
const crypto = require('crypto');

class ScellClient {
  constructor({ apiKey, environment = 'production' }) {
    this.apiKey = apiKey;
    this.baseUrl = environment === 'sandbox'
      ? 'https://api.scell.io/api/v1/sandbox'
      : 'https://api.scell.io/api/v1';
  }

  async request(method, path, body = null) {
    const options = {
      method,
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json',
      },
    };

    if (body) {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(`${this.baseUrl}${path}`, options);

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      const err = new Error(error.message || `HTTP ${response.status}`);
      err.status = response.status;
      err.errors = error.errors;
      throw err;
    }

    return response.json();
  }

  // Factures
  async createInvoice(data) {
    return this.request('POST', '/invoices', data);
  }

  async getInvoice(id) {
    return this.request('GET', `/invoices/${id}`);
  }

  async submitInvoice(id) {
    return this.request('POST', `/invoices/${id}/submit`);
  }

  async downloadInvoice(id, type = 'pdf') {
    return this.request('GET', `/invoices/${id}/download/${type}`);
  }

  // Signatures
  async createSignature(data) {
    return this.request('POST', '/signatures', data);
  }

  async getSignature(id) {
    return this.request('GET', `/signatures/${id}`);
  }

  async remindSignature(id) {
    return this.request('POST', `/signatures/${id}/remind`);
  }

  async cancelSignature(id) {
    return this.request('POST', `/signatures/${id}/cancel`);
  }

  async downloadSigned(id) {
    return this.request('GET', `/signatures/${id}/download/signed`);
  }
}

module.exports = ScellClient;

// Utilisation
// const client = new ScellClient({
//   apiKey: process.env.SCELL_API_KEY,
//   environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
// });
// const invoice = await client.createInvoice({ ... });

Application PHP / Laravel

Un service Laravel complet :

php
<?php
// app/Services/ScellService.php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Facades\Log;

class ScellService
{
    private Client $client;

    public function __construct()
    {
        $environment = config('services.scell.environment', 'sandbox');
        $baseUri = $environment === 'sandbox'
            ? 'https://api.scell.io/api/v1/sandbox/'
            : 'https://api.scell.io/api/v1/';

        $this->client = new Client([
            'base_uri' => $baseUri,
            'headers' => [
                'X-API-Key' => config('services.scell.api_key'),
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'timeout' => 30,
        ]);
    }

    // --- Factures ---

    public function createInvoice(array $data): array
    {
        return $this->post('invoices', $data);
    }

    public function getInvoice(string $id): array
    {
        return $this->get("invoices/{$id}");
    }

    public function submitInvoice(string $id): array
    {
        return $this->post("invoices/{$id}/submit");
    }

    public function downloadInvoice(string $id, string $type = 'pdf'): array
    {
        return $this->get("invoices/{$id}/download/{$type}");
    }

    // --- Signatures ---

    public function createSignature(array $data): array
    {
        return $this->post('signatures', $data);
    }

    public function getSignature(string $id): array
    {
        return $this->get("signatures/{$id}");
    }

    public function remindSignature(string $id): array
    {
        return $this->post("signatures/{$id}/remind");
    }

    public function cancelSignature(string $id): array
    {
        return $this->post("signatures/{$id}/cancel");
    }

    // --- HTTP helpers ---

    private function get(string $uri): array
    {
        try {
            $response = $this->client->get($uri);
            return json_decode($response->getBody(), true);
        } catch (ClientException $e) {
            return $this->handleError($e);
        }
    }

    private function post(string $uri, array $data = []): array
    {
        try {
            $response = $this->client->post($uri, [
                'json' => $data,
            ]);
            return json_decode($response->getBody(), true);
        } catch (ClientException $e) {
            return $this->handleError($e);
        }
    }

    private function handleError(ClientException $e): array
    {
        $response = $e->getResponse();
        $body = json_decode($response->getBody(), true);

        Log::error('Scell API error', [
            'status' => $response->getStatusCode(),
            'body' => $body,
        ]);

        throw new \RuntimeException(
            $body['message'] ?? 'Erreur API Scell.io',
            $response->getStatusCode()
        );
    }
}

Configuration Laravel :

php
// config/services.php
return [
    // ...
    'scell' => [
        'api_key' => env('SCELL_API_KEY'),
        'webhook_secret' => env('SCELL_WEBHOOK_SECRET'),
        'environment' => env('SCELL_ENVIRONMENT', 'sandbox'),
    ],
];

Script Python

Un module Python reutilisable :

python
# scell_client.py
import os
import time
import hmac
import hashlib
import requests
from typing import Optional

class ScellClient:
    def __init__(self, api_key: Optional[str] = None, environment: str = 'sandbox'):
        self.api_key = api_key or os.environ['SCELL_API_KEY']
        self.base_url = (
            'https://api.scell.io/api/v1/sandbox'
            if environment == 'sandbox'
            else 'https://api.scell.io/api/v1'
        )
        self.session = requests.Session()
        self.session.headers.update({
            'X-API-Key': self.api_key,
            'Content-Type': 'application/json',
        })

    def _request(self, method: str, path: str, json_data: dict = None) -> dict:
        url = f'{self.base_url}{path}'
        response = self.session.request(method, url, json=json_data)
        response.raise_for_status()
        return response.json()

    # --- Factures ---

    def create_invoice(self, data: dict) -> dict:
        return self._request('POST', '/invoices', data)

    def get_invoice(self, invoice_id: str) -> dict:
        return self._request('GET', f'/invoices/{invoice_id}')

    def submit_invoice(self, invoice_id: str) -> dict:
        return self._request('POST', f'/invoices/{invoice_id}/submit')

    def download_invoice(self, invoice_id: str, file_type: str = 'pdf') -> dict:
        return self._request('GET', f'/invoices/{invoice_id}/download/{file_type}')

    # --- Signatures ---

    def create_signature(self, data: dict) -> dict:
        return self._request('POST', '/signatures', data)

    def get_signature(self, signature_id: str) -> dict:
        return self._request('GET', f'/signatures/{signature_id}')

    def remind_signature(self, signature_id: str) -> dict:
        return self._request('POST', f'/signatures/{signature_id}/remind')

    def cancel_signature(self, signature_id: str) -> dict:
        return self._request('POST', f'/signatures/{signature_id}/cancel')

    # --- Verification de webhook ---

    @staticmethod
    def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
        parts = dict(p.split('=', 1) for p in signature_header.split(','))
        timestamp = parts.get('t', '')
        received_sig = parts.get('v1', '')

        # Verifier le timestamp (5 minutes max)
        if abs(time.time() - int(timestamp)) > 300:
            return False

        # Calculer la signature attendue
        signed_payload = f'{timestamp}.{payload.decode()}'
        expected_sig = hmac.new(
            secret.encode(),
            signed_payload.encode(),
            hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(expected_sig, received_sig)


# Utilisation :
# client = ScellClient(environment='sandbox')
# invoice = client.create_invoice({...})
# print(f"Facture creee : {invoice['data']['id']}")

Recapitulatif

Voici les endpoints principaux que vous utiliserez au quotidien :

ActionMethodeEndpoint
Consulter le soldeGET/v1/balance
Creer une facturePOST/v1/invoices
Voir une factureGET/v1/invoices/{id}
Soumettre une facturePOST/v1/invoices/{id}/submit
Telecharger un PDFGET/v1/invoices/{id}/download/pdf
Creer une signaturePOST/v1/signatures
Voir une signatureGET/v1/signatures/{id}
Envoyer un rappelPOST/v1/signatures/{id}/remind
Annuler une signaturePOST/v1/signatures/{id}/cancel
Telecharger le signeGET/v1/signatures/{id}/download/signed
Creer un webhookPOST/v1/webhooks
Tester un webhookPOST/v1/webhooks/{id}/test

Et ensuite ?

Besoin d'aide ? Contactez notre equipe sur support@scell.io ou consultez la documentation API complete.

Documentation Scell.io