openapi: 3.1.0 info: title: API Project 404 (site vitrine) version: '1.0.0' description: | Deux sens d'échange entre le site vitrine et le back-office / espace client : 1. **Lecture** (`/api/v1/*.json`) : contenus publics du site, générés à chaque mise en ligne. Méthode GET, sans authentification, CORS ouvert. 2. **Webhook `leads`** : le site envoie chaque demande (contact, audit, support) en POST vers `{PUBLIC_API_URL}/leads`. C'est le back-office qui implémente cet endpoint. Toutes les réponses de lecture ont la forme `{ apiVersion, generatedAt, data, ... }`. Documentation complète : docs/API.md dans le dépôt du site. servers: - url: https://project404.fr description: Site vitrine (API en lecture) paths: /api/v1/index.json: get: summary: Liste des ressources disponibles responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/site.json: get: summary: Coordonnées et informations générales responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/services.json: get: summary: Services (avec familles, livrables et FAQ) responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/solutions.json: get: summary: Solutions par secteur responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/plans.json: get: summary: Plans d'hébergement et prix (HT, mensuels) responses: '200': description: OK content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/Plan' } } /api/v1/faqs.json: get: summary: FAQ par page responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/blog.json: get: summary: Articles du blog (métadonnées et FAQ) responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/aide.json: get: summary: Articles du centre d'aide (avec le texte Markdown) description: Pratique pour afficher l'aide directement dans l'espace client. responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/formations.json: get: summary: Formations, chapitres et leçons (sans les réponses des QCM) description: "Chaque leçon a un gabarit (format) : cours, video, pdf ou quiz, son chapitre et son numéro (« 2.1 »)." responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/legal.json: get: summary: Documents légaux en vigueur (titre, adresse, version, dates, rubriques) description: "Rubriques : hebergement, creation, seo, site. Le texte de chaque document est sur sa page (url)." responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } /api/v1/jobs.json: get: summary: Offres d'emploi, de stage et d'alternance en cours (avec le texte Markdown) description: "Types de contrat : stage, alternance, cdi, cdd, freelance. Organisation : sur-place, hybride, teletravail." responses: '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } webhooks: leads: post: summary: Nouvelle demande envoyée par le site (à implémenter côté back-office) description: | URL appelée : `POST {PUBLIC_API_URL}/leads`, en JSON, depuis le navigateur du visiteur. Le back-office doit autoriser l'origine du site (CORS) et répondre en JSON. Réponse 2xx = succès ; un `message` ou une `reference` renvoyés sont affichés au visiteur. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/Lead' } examples: contact: value: version: 1 type: contact submittedAt: '2026-11-09T10:12:00.000Z' contact: { firstName: Claire, lastName: Martin, email: claire@exemple.fr, phone: '0600000000', company: Boulangerie Martin } fields: { sujet: Refonte de site, message: 'Bonjour, notre site est lent sur mobile...' } source: { site: 'https://project404.fr', page: /contact, intent: starter-402, landing: /tarifs, utm: { source: google, medium: cpc } } consent: { contact: true, text: 'En envoyant ce formulaire, vous acceptez que vos données soient utilisées pour vous recontacter.' } support: value: version: 1 type: support submittedAt: '2026-11-09T10:12:00.000Z' contact: { fullName: Claire Martin, email: claire@exemple.fr } fields: { site: 'https://www.boulangerie-martin.fr', type: Incident, urgence: Site hors ligne, message: 'Le site affiche une erreur 500 depuis ce matin.' } source: { site: 'https://project404.fr', page: /aide } consent: { contact: true, text: '...' } responses: '201': description: Demande enregistrée content: application/json: schema: type: object properties: reference: { type: string, example: 'P404-2026-0142' } message: { type: string, example: 'Merci ! Votre demande P404-2026-0142 est enregistrée.' } '400': { description: Données invalides } '429': { description: Trop de demandes (limite anti-abus) } candidatures: post: summary: Candidature envoyée par le site, avec CV en PDF description: | URL appelée : `POST {PUBLIC_API_URL}/candidatures`, en `multipart/form-data`, depuis le navigateur du candidat (pages /carrieres/ et /carrieres/candidature). Format des champs : packages/shared/src/application.ts. CV en PDF, 5 Mo au plus ; requête entière 6 Mo au plus. requestBody: required: true content: multipart/form-data: schema: type: object required: [kind, firstName, lastName, email, message, cv] properties: jobSlug: { type: string, description: 'Offre visée (absente pour une candidature spontanée)', example: stage-developpeur-web } jobTitle: { type: string, example: Stage développeur web } kind: { type: string, enum: [emploi, stage, alternance] } firstName: { type: string } lastName: { type: string } email: { type: string, format: email } phone: { type: string } city: { type: string } availability: { type: string } duration: { type: string, description: Stage ou alternance } school: { type: string, description: Stage ou alternance } link: { type: string, description: 'LinkedIn, portfolio ou GitHub' } message: { type: string } cv: { type: string, format: binary, description: 'PDF, 5 Mo au plus' } source: { type: string, description: 'JSON : site, page, landing, referrer, utm' } consent: { type: string, description: Texte de consentement affiché au candidat } responses: '201': description: Candidature enregistrée (accusé de réception envoyé au candidat) content: application/json: schema: type: object properties: reference: { type: string, example: 'C-2026-0001' } message: { type: string } '400': { description: 'Données invalides, CV manquant ou fichier qui n''est pas un PDF' } '403': { description: Origine non autorisée } '413': { description: CV ou requête trop lourds } '429': { description: Trop de candidatures (limite anti-abus) } components: schemas: Envelope: type: object required: [apiVersion, generatedAt, data] properties: apiVersion: { type: string, example: v1 } generatedAt: { type: string, format: date-time } data: {} Plan: type: object properties: id: { type: string, example: starter-402 } name: { type: string, example: Starter 402 } tagline: { type: string } currency: { type: string, example: EUR } monthlyPriceExclTax: { type: [number, 'null'], example: 28 } compareAtExclTax: { type: [number, 'null'] } from: { type: [string, 'null'] } featured: { type: boolean } features: { type: array, items: { type: string } } orderUrl: { type: string, format: uri } Lead: type: object required: [version, type, submittedAt, contact, fields, source, consent] properties: version: { type: integer, const: 1 } type: { type: string, enum: [contact, audit, report, support, formation, test], description: 'report = suite à un rapport d’audit (rappel, devis, rendez-vous)' } submittedAt: { type: string, format: date-time } contact: type: object required: [email] properties: firstName: { type: string } lastName: { type: string } fullName: { type: string } email: { type: string, format: email } phone: { type: string } company: { type: string } fields: type: object description: Autres champs du formulaire (sujet, message, estimation, site, analyse, objectif, besoin, creneau, offre, rapport, urgence, type...) additionalProperties: { type: string } source: type: object required: [site, page] properties: site: { type: string, format: uri } page: { type: string } intent: { type: string, description: 'Valeur de ?sujet= dans l’URL (plan choisi, rapport d’audit...)' } landing: { type: string, description: Première page vue pendant la visite } referrer: { type: string, description: Site d’où vient le visiteur } utm: { type: object, additionalProperties: { type: string } } consent: type: object properties: contact: { type: boolean } text: { type: string }