Aller au contenu principal
Version: 2.1

Webhook - Notification de commande

Endpoint à implémenter

Vous devez créer un endpoint HTTP qui accepte des POST requests en JSON.


Format de la requête

Méthode : POST

Headers :

Content-Type: application/json
User-Agent: ComptAppart-Webhook/1.0
X-API-KEY: <notre_clé_api>

Body (JSON) :

{
"event_type": "order.created",
"event_id": "evt_1234567890",
"order_id": 42,
"timestamp": "2025-12-05T14:30:00Z",

"customer": {
"first_name": "Jean",
"last_name": "Dupont",
"email": "jean.dupont@example.com",
"phone": "+33612345678"
},

"order": {
"total_amount": 49.99,
"currency": "EUR",
"payment_date": "2025-12-05T14:30:00Z",
"products": [
{
"product_id": "prod_QLower123",
"product_name": "Service Fiscalité Premium",
"quantity": 1,
"unit_price": 49.99
}
]
},

"invoice": {
"pdf_url": "https://s3.amazonaws.com/.../invoice_42.pdf",
"number": "INV-2025-001234"
}
}

Champs détaillés

Champ event_type

Type d'événement. Pour l'instant uniquement :

  • "order.created" : Nouvelle commande créée

Champ event_id

ID unique de l'événement Stripe (pour traçabilité).

Champ order_id

ID unique de la commande dans notre système ComptAppart.

Champ timestamp

Date/heure de l'événement au format ISO 8601 avec timezone UTC.

Objet customer

Informations du client ayant effectué le paiement.

ChampTypeDescriptionNullable
first_namestringPrénom du clientOui
last_namestringNom du clientOui
emailstringEmail du clientNon
phonestringTéléphone au format internationalOui

⚠️ Note : first_name et last_name peuvent être vides si le client n'a pas fourni ces informations lors du paiement.

Objet order

Détails de la commande.

ChampTypeDescription
total_amountfloatMontant total payé (incluant TVA)
currencystringCode devise ISO 4217 (ex: "EUR")
payment_datestringDate du paiement (ISO 8601)
productsarrayListe des produits achetés

Array products[]

Liste des produits de la commande.

ChampTypeDescription
product_idstringID du produit Stripe
product_namestringNom du produit
quantityintegerQuantité achetée
unit_pricefloatPrix unitaire HT

Objet invoice

Informations sur la facture générée.

ChampTypeDescription
pdf_urlstringURL publique de la facture PDF (valide 24h)
numberstringNuméro de facture unique

Exemple de payload complet

{
"event_type": "order.created",
"event_id": "evt_3QMqH82eZvKYlo2C0xzQjKVp",
"order_id": 156,
"timestamp": "2025-12-05T14:32:18.000Z",

"customer": {
"first_name": "Marie",
"last_name": "Martin",
"email": "marie.martin@gmail.com",
"phone": "+33687654321"
},

"order": {
"total_amount": 99.99,
"currency": "EUR",
"payment_date": "2025-12-05T14:32:18.000Z",
"products": [
{
"product_id": "prod_RFvQB6zA3Tn8yZ",
"product_name": "Déclaration d'impôts - Formule Complète",
"quantity": 1,
"unit_price": 83.32
}
]
},

"invoice": {
"pdf_url": "https://qlower-documents.s3.eu-west-3.amazonaws.com/partner_invoices/invoice_156_20251205_143218.pdf",
"number": "FQCA-2025-000156"
}
}

Exemples d'implémentation

Node.js / Express

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

app.use(express.json());

app.post('/api/comptappart/orders', async (req, res) => {
try {
const { event_type, order_id, customer, order, invoice } = req.body;

// 1. Vérifier l'API key
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.COMPTAPPART_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}

// 2. Vérifier le type d'événement
if (event_type !== 'order.created') {
return res.status(400).json({ error: 'Unknown event type' });
}

// 3. Gérer l'idempotence (important pour les retries)
const existingOrder = await Order.findOne({ comptappart_order_id: order_id });
if (existingOrder) {
return res.status(200).json({ success: true, message: 'Already processed' });
}

// 4. Sauvegarder dans votre base de données
await Order.create({
comptappart_order_id: order_id,
customer_email: customer.email,
customer_name: `${customer.first_name} ${customer.last_name}`.trim(),
amount: order.total_amount,
currency: order.currency,
products: order.products,
invoice_url: invoice.pdf_url,
status: 'completed'
});

// 5. Déclencher vos process métier
await activateService(customer.email, order.products);

// 6. Répondre avec succès
res.status(200).json({ success: true });

} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});

app.listen(3000, () => console.log('Webhook endpoint ready'));

Python / Flask

from flask import Flask, request, jsonify
import os
import logging

app = Flask(__name__)
logger = logging.getLogger(__name__)

@app.route('/api/comptappart/orders', methods=['POST'])
def handle_comptappart_order():
try:
# 1. Vérifier l'API key
api_key = request.headers.get('X-API-KEY')
if api_key != os.environ.get('COMPTAPPART_API_KEY'):
return jsonify({'error': 'Unauthorized'}), 401

# 2. Parser le payload
data = request.get_json()
event_type = data.get('event_type')
order_id = data.get('order_id')
customer = data.get('customer', {})
order = data.get('order', {})
invoice = data.get('invoice', {})

# 3. Vérifier le type d'événement
if event_type != 'order.created':
return jsonify({'error': 'Unknown event type'}), 400

# 4. Gérer l'idempotence (important pour les retries)
existing_order = Order.query.filter_by(comptappart_order_id=order_id).first()
if existing_order:
return jsonify({'success': True, 'message': 'Already processed'}), 200

# 5. Sauvegarder dans votre base de données
new_order = Order(
comptappart_order_id=order_id,
customer_email=customer.get('email'),
customer_name=f"{customer.get('first_name', '')} {customer.get('last_name', '')}".strip(),
amount=order.get('total_amount'),
currency=order.get('currency'),
products=order.get('products', []),
invoice_url=invoice.get('pdf_url'),
status='completed'
)
db.session.add(new_order)
db.session.commit()

# 6. Déclencher vos process métier
activate_service(customer.get('email'), order.get('products'))

# 7. Répondre avec succès
return jsonify({'success': True}), 200

except Exception as e:
logger.error(f"Webhook error: {e}", exc_info=True)
return jsonify({'error': 'Internal Server Error'}), 500

if __name__ == '__main__':
app.run(port=3000)

Codes de réponse

CodeDescriptionComportement ComptAppart
200-299SuccèsMarque la notification comme complétée
400-499Erreur client (ex: format invalide)Pas de retry, marque comme échouée
500-599Erreur serveurRetry automatique (3 tentatives max)
TimeoutPas de réponse en 10sRetry automatique

Retry policy

En cas d'échec (5xx ou timeout), ComptAppart retentera automatiquement :

  • 🔄 Tentative 1 : Immédiate
  • 🔄 Tentative 2 : Après 5 minutes
  • 🔄 Tentative 3 : Après 30 minutes

Après 3 échecs, la notification est marquée comme définitivement échouée et vous devrez nous contacter pour investigation.


Idempotence

Le même webhook peut être envoyé plusieurs fois en cas de retry. Vous devez gérer l'idempotence côté serveur :

# Exemple : vérifier si la commande existe déjà
existing_order = Order.objects.filter(
comptappart_order_id=order_id
).first()

if existing_order:
# Commande déjà traitée, retourner succès sans rien faire
return jsonify({'success': True, 'message': 'Already processed'}), 200