{"openapi":"3.0.3","info":{"title":"ReachFlow API publique","version":"1.0.0","description":"API REST v1 — messages transactionnels WhatsApp, OTP, providers. Authentification : en-tête `X-API-Key`.\n\n## ⚠️ Protection anti-bannissement (montée en puissance)\n\nWhatsApp bannit les numéros qui envoient trop, trop vite, surtout les numéros récents. Pour protéger vos numéros, **chaque envoi via l'API respecte par défaut la montée en puissance (warm-up)** de l'instance, exactement comme les campagnes : un plafond quotidien progressif (sur 14 jours) et un plafond de nouveaux contacts par jour.\n\nQuand un plafond est atteint, le message est mis en file puis **passe en `failed` avec `failureCode = warmup_daily_limit`** (il n'est pas envoyé). Vous le constatez via le webhook `message.failed` ou `GET /messages/{id}`.\n\nDeux protections sont **toujours actives et NON désactivables** :\n- **Plancher numéros récents** : un numéro encore en début de warm-up reste plafonné même si vous désactivez la protection.\n- **Disjoncteur de risque** : si le score de risque d'un numéro devient critique (blocages détectés), les envois sont suspendus (`failureCode = risk_circuit_open`) jusqu'au rétablissement.\n\n## Désactiver la montée en puissance (à vos risques)\n\nPour un numéro mature à fort volume transactionnel (OTP, notifications consenties), vous pouvez **lever le plafonnement warm-up par numéro** depuis le tableau de bord : **Instances → (votre numéro) → Politique d'envoi API → désactiver la montée en puissance**. La désactivation exige une confirmation explicite d'acceptation du risque et est tracée (qui / quand).\n\n> **Avertissement** : désactiver la montée en puissance peut entraîner le **bannissement définitif du numéro par WhatsApp** en cas d'abus ou de volume trop élevé. Vous restez responsable du consentement de vos destinataires."},"servers":[{"url":"/api/v1","description":"Instance ReachFlow"}],"tags":[{"name":"Providers","description":"Numéros WhatsApp (channel_providers) connectés."},{"name":"Messages","description":"Envoi transactionnel et statut."},{"name":"OTP","description":"Codes à usage unique par WhatsApp."},{"name":"Content","description":"Extraction de contenu web (sans indexation RAG)."},{"name":"Scrape","description":"Extraction structurée de données depuis des pages web (Phase 9p)."}],"paths":{"/providers":{"get":{"tags":["Providers"],"operationId":"listProviders","summary":"Lister tous les providers","description":"Retourne la liste des numéros WhatsApp de votre organisation accessibles via l’API.","x-reachflow-id":"list-all-providers","x-reachflow-scope":"providers:read","security":[{"ApiKey":[]}],"responses":{"200":{"description":"Liste des providers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProviderList"},"example":{"providers":[{"id":"a3f1b2c4-1234-4abc-9def-000011112222","name":"Support client","phoneNumber":"22997000000","status":"connected","warmupDay":14,"riskScore":12}]}}}}}}},"/providers/{id}":{"get":{"tags":["Providers"],"operationId":"getProvider","summary":"Afficher un provider","description":"Détail d’un provider avec statistiques journalières.","x-reachflow-id":"get-provider","x-reachflow-scope":"providers:read","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant du provider."}],"responses":{"200":{"description":"Provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProviderDetail"}}}}}}},"/messages/send":{"post":{"tags":["Messages"],"operationId":"sendMessage","summary":"Envoyer un message texte","description":"Met en file un message texte vers un destinataire. Le provider doit être connecté. L'envoi respecte par défaut la montée en puissance du numéro (voir « Protection anti-bannissement ») : en cas de plafond atteint, le message passe en `failed` avec `failureCode = warmup_daily_limit`. Consultez le statut via `GET /messages/{id}` ou le webhook `message.failed`.","x-reachflow-id":"send-message","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Clé d’idempotence optionnelle."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendMessage"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","to":"22996123456","message":"Bonjour depuis ReachFlow !"}}}},"responses":{"202":{"description":"Message accepté en file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendAccepted"}}}}}}},"/messages/send-media":{"post":{"tags":["Messages"],"operationId":"sendMedia","summary":"Envoyer un média","description":"Envoie une image, vidéo, audio ou document via URL publique. `mediaUrl` doit être en **HTTPS**, accessible publiquement (les adresses internes/privées sont refusées), de taille ≤ 16 Mo et d'un type cohérent avec `mediaType` — sinon `400`. Soumis à la montée en puissance du numéro (voir « Protection anti-bannissement »).","x-reachflow-id":"send-media","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendMedia"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","to":"22996123456","mediaUrl":"https://example.com/photo.jpg","mediaType":"image","caption":"Votre reçu"}}}},"responses":{"202":{"description":"Message accepté","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendAccepted"}}}}}}},"/messages/send-bulk":{"post":{"tags":["Messages"],"operationId":"sendBulk","summary":"Envoi en lot","description":"Envoie le même modèle à plusieurs destinataires. Les refus partiels sont listés dans rejections. Chaque message reste soumis à la montée en puissance du numéro : au-delà du plafond quotidien, les messages excédentaires passent en `failed` (`warmup_daily_limit`) — voir « Protection anti-bannissement ».","x-reachflow-id":"send-bulk","x-reachflow-scope":"messages:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendBulk"}}}},"responses":{"200":{"description":"Résultat du lot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSendBulkResponse"}}}}}}},"/messages/{id}":{"get":{"tags":["Messages"],"operationId":"getMessageStatus","summary":"Statut d’un message","description":"Consulte le cycle de vie d’un message API précédemment accepté.","x-reachflow-id":"get-message-status","x-reachflow-scope":"messages:read","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant message (messageId)."}],"responses":{"200":{"description":"Statut","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMessageStatus"}}}}}}},"/otp/send":{"post":{"tags":["OTP"],"operationId":"sendOtp","summary":"Envoyer un OTP","description":"Génère un code et l’envoie au numéro indiqué. Par défaut le message utilise le nom du tenant (`{{brand}}`) ; `brandName` permet de le surcharger (marque de votre app). L'envoi OTP respecte la montée en puissance du numéro ; pour de gros volumes OTP sur un numéro mature, désactivez-la par numéro depuis le dashboard (à vos risques — voir « Protection anti-bannissement »).","x-reachflow-id":"send-otp","x-reachflow-scope":"otp:send","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpSend"},"example":{"providerId":"a3f1b2c4-1234-4abc-9def-000011112222","phoneNumber":"22996123456","brandName":"Mon App","codeLength":6,"expiresIn":300}}}},"responses":{"200":{"description":"OTP créé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpSendResponse"}}}}}}},"/otp/verify":{"post":{"tags":["OTP"],"operationId":"verifyOtp","summary":"Vérifier un OTP","description":"Valide le code saisi par l’utilisateur final.","x-reachflow-id":"verify-otp","x-reachflow-scope":"otp:verify","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpVerify"},"example":{"otpId":"c4d5e6f7-8901-42ab-cdef-333344445555","code":"482910"}}}},"responses":{"200":{"description":"Résultat de vérification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOtpVerifyResponse"},"example":{"valid":true}}}}}}},"/content/extract":{"post":{"tags":["Content"],"operationId":"startContentExtract","summary":"Extraire le contenu d’une URL","description":"Lance une extraction asynchrone du texte d’une page web (sans indexation RAG). Nécessite le scope `content:extract` et l’activation du scraping sur l’instance. Consultez le résultat via `GET /content/extract/{jobId}`.","x-reachflow-id":"start-content-extract","x-reachflow-scope":"content:extract","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicContentExtractRequest"},"example":{"url":"https://example.com/article"}}}},"responses":{"200":{"description":"Extraction acceptée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicContentExtractJobResponse"}}}}}}},"/content/extract/{jobId}":{"get":{"tags":["Content"],"operationId":"getContentExtract","summary":"Résultat d’une extraction","description":"Consulte le statut et le texte extrait d’un job lancé via POST /content/extract.","x-reachflow-id":"get-content-extract","x-reachflow-scope":"content:extract","security":[{"ApiKey":[]}],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant du job d’extraction."}],"responses":{"200":{"description":"Résultat","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicContentExtractResult"}}}}}}},"/scrape/extract":{"post":{"tags":["Scrape"],"operationId":"startScrapeExtract","summary":"Extraire des enregistrements structurés depuis une page web","description":"Lance une extraction asynchrone d'enregistrements typés depuis une URL ou du texte collé. Nécessite le scope `scrape:extract` et un plan PRO ou supérieur (feature scrape_collect). Retourne un jobId permettant de récupérer les enregistrements JSON extraits. Bornes one-shot : 5 items max, 1 page, 5 items LLM max. Consultez le résultat via `GET /scrape/extract/{jobId}`.","x-reachflow-id":"start-scrape-extract","x-reachflow-scope":"scrape:extract","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicScrapeExtractRequest"},"example":{"url":"https://opportunitydesk.org/scholarships/","targetSchema":"OPPORTUNITY","customFields":[{"key":"deadline","description":"Date limite de candidature"}],"followDetailPages":false}}}},"responses":{"200":{"description":"Extraction acceptée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicScrapeExtractJobResponse"}}}},"403":{"description":"Plan insuffisant ou feature non activée"}}}},"/scrape/extract/{jobId}":{"get":{"tags":["Scrape"],"operationId":"getScrapeExtract","summary":"Résultat extraction structurée","description":"Retourne le statut et les enregistrements JSON extraits depuis un job lancé via POST /scrape/extract. Le statut peut être PENDING, READY ou FAILED.","x-reachflow-id":"get-scrape-extract","x-reachflow-scope":"scrape:extract","security":[{"ApiKey":[]}],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identifiant du job extraction."}],"responses":{"200":{"description":"Résultat extraction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicScrapeExtractResult"}}}},"404":{"description":"Job introuvable ou expiré"}}}},"/scrape/sources":{"get":{"tags":["Scrape"],"operationId":"listScrapeSources","summary":"Lister les sources de collecte","description":"Liste paginée des sources persistées. Scope `scrape:sources`. Clés live uniquement (sandbox refusé). Plan PRO+.","x-reachflow-id":"list-scrape-sources","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Liste des sources"},"400":{"description":"Instance sandbox : sources persistées réservées aux clés live"}}},"post":{"tags":["Scrape"],"operationId":"createScrapeSource","summary":"Créer une source de collecte","description":"Crée une source (URL seed + preset + recette). Scope `scrape:sources`. Clés live uniquement.","x-reachflow-id":"create-scrape-source","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScrapeSource"},"example":{"name":"Stages Opportunity Desk","seedUrl":"https://opportunitydesk.org/category/jobs-and-internships/internships/","targetSchema":"OPPORTUNITY","schedule":"MANUAL"}}}},"responses":{"201":{"description":"Source créée"},"400":{"description":"Validation ou sandbox"},"403":{"description":"Plan insuffisant ou quota sources atteint"}}}},"/scrape/sources/{id}":{"get":{"tags":["Scrape"],"operationId":"getScrapeSource","summary":"Détail d'une source","x-reachflow-id":"get-scrape-source","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Source"},"404":{"description":"Introuvable"}}},"patch":{"tags":["Scrape"],"operationId":"updateScrapeSource","summary":"Mettre à jour une source","x-reachflow-id":"update-scrape-source","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScrapeSource"}}}},"responses":{"200":{"description":"Source mise à jour"}}},"delete":{"tags":["Scrape"],"operationId":"deleteScrapeSource","summary":"Supprimer une source et ses records","x-reachflow-id":"delete-scrape-source","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Supprimée"}}}},"/scrape/sources/{id}/run":{"post":{"tags":["Scrape"],"operationId":"runScrapeSource","summary":"Lancer un run manuel sur une source","description":"Publie un job d'extraction persisté (dédup records). Retourne jobId (poll GET /scrape/extract/{jobId}). Clés live uniquement.","x-reachflow-id":"run-scrape-source","x-reachflow-scope":"scrape:sources","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Run accepté","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicScrapeExtractJobResponse"}}}}}}},"/scrape/records":{"get":{"tags":["Scrape"],"operationId":"listScrapeRecords","summary":"Lister les records collectés","description":"Records paginés. Filtres sourceId, status (NEW|SEEN|ARCHIVED), since. Scope `scrape:records`. Clés live uniquement.","x-reachflow-id":"list-scrape-records","x-reachflow-scope":"scrape:records","security":[{"ApiKey":[]}],"parameters":[{"name":"sourceId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string","enum":["NEW","SEEN","ARCHIVED"]}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Liste des records"},"400":{"description":"Instance sandbox"}}}}},"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Clé API sandbox (rfl_sandbox_) ou live (rfl_live_) — type imposé par l’instance."}},"schemas":{"PublicSendMessage":{"type":"object","required":["providerId","to","message"],"properties":{"providerId":{"type":"string","format":"uuid","description":"Provider expéditeur."},"to":{"type":"string","description":"Numéro destinataire (8–20 car.)."},"message":{"type":"string","description":"Corps du message (1–4096 car.)."},"variables":{"type":"object","additionalProperties":{"type":"string"},"description":"Variables pour modèle {{clé}}."},"scheduleAt":{"type":"string","format":"date-time","description":"Envoi différé optionnel."},"saveContact":{"type":"boolean","default":false,"description":"Si true, enregistre le destinataire dans la base contacts (find-or-create). Par défaut : envoi direct sans création ni quota contacts."}}},"PublicSendMedia":{"type":"object","required":["providerId","to","mediaUrl","mediaType"],"properties":{"providerId":{"type":"string","format":"uuid"},"to":{"type":"string"},"mediaUrl":{"type":"string","format":"uri","description":"URL HTTPS publique du média (hôtes internes/privés refusés ; ≤ 16 Mo ; type cohérent avec mediaType)."},"mediaType":{"enum":["image","document","audio","video"]},"caption":{"type":"string"},"saveContact":{"type":"boolean","default":false}}},"PublicSendBulk":{"type":"object","required":["providerId","messageTemplate","recipients"],"properties":{"providerId":{"type":"string","format":"uuid"},"messageTemplate":{"type":"string"},"recipients":{"type":"array","items":{"type":"object","required":["to"],"properties":{"to":{"type":"string"},"variables":{"type":"object","additionalProperties":{"type":"string"}}}}},"scheduleAt":{"type":"string","format":"date-time"},"saveContact":{"type":"boolean","default":false}}},"PublicSendAccepted":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"queuedAt":{"type":"string","format":"date-time"}}},"PublicSendBulkResponse":{"type":"object","properties":{"bulkId":{"type":"string","format":"uuid"},"accepted":{"type":"integer"},"rejected":{"type":"integer"},"rejections":{"type":"array","items":{"type":"object"}},"messageIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"PublicMessageStatus":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","sent","delivered","failed","cancelled"]},"to":{"type":"string"},"providerId":{"type":"string","format":"uuid"},"queuedAt":{"type":"string","format":"date-time"},"sentAt":{"type":"string","format":"date-time","nullable":true},"deliveredAt":{"type":"string","format":"date-time","nullable":true},"failedAt":{"type":"string","format":"date-time","nullable":true},"failureCode":{"type":"string","nullable":true,"enum":["warmup_daily_limit","risk_circuit_open","provider_disconnected","provider_banned","send_not_allowed","instance_busy","delivery_timeout","delivery_failed","send_error","provider_not_found"],"description":"Cause d’échec : `warmup_daily_limit` (plafond montée en puissance — désactivable par numéro depuis le dashboard, à vos risques) · `risk_circuit_open` (numéro suspendu, score de risque critique — non désactivable) · `provider_disconnected` / `provider_banned` (numéro non connecté ou banni) · `send_not_allowed` (numéro non autorisé en environnement restreint) · `instance_busy` (instance saturée, réessayez) · `delivery_timeout` (non confirmé par WhatsApp dans le délai) · `delivery_failed` (échec de livraison signalé par WhatsApp)."},"failureReason":{"type":"string","nullable":true}}},"PublicProviderList":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/PublicProviderSummary"}}}},"PublicProviderSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"phoneNumber":{"type":"string","nullable":true},"status":{"type":"string"},"warmupDay":{"type":"integer"},"riskScore":{"type":"integer"}}},"PublicProviderDetail":{"allOf":[{"$ref":"#/components/schemas/PublicProviderSummary"},{"type":"object","properties":{"dailyStats":{"type":"object"}}}]},"PublicOtpSend":{"type":"object","required":["providerId","phoneNumber"],"properties":{"providerId":{"type":"string","format":"uuid"},"phoneNumber":{"type":"string"},"codeLength":{"type":"integer","default":6},"expiresIn":{"type":"integer","default":300},"brandName":{"type":"string","maxLength":64,"description":"Nom affiché dans le message (marque de votre application). Si omis : nom de l’organisation ReachFlow du tenant."},"template":{"type":"string","description":"Modèle personnalisé. Placeholders : {{brand}}, {{code}}. Défaut : « Votre code de vérification {{brand}} : {{code}} »."},"saveContact":{"type":"boolean","default":false}}},"PublicOtpSendResponse":{"type":"object","properties":{"otpId":{"type":"string","format":"uuid"},"messageId":{"type":"string","format":"uuid","description":"Message WhatsApp en file — le code n’est pas renvoyé par l’API (voir statut sent/failed)."},"expiresAt":{"type":"string","format":"date-time"}}},"PublicOtpVerify":{"type":"object","required":["otpId","code"],"properties":{"otpId":{"type":"string","format":"uuid"},"code":{"type":"string"}}},"PublicOtpVerifyResponse":{"type":"object","properties":{"valid":{"type":"boolean"},"reason":{"type":"string"},"attemptsLeft":{"type":"integer"}}},"PublicContentExtractRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL HTTPS publique à extraire (hôtes internes/privés refusés)."}}},"PublicContentExtractJobResponse":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING"]}}},"PublicContentExtractResult":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","READY","FAILED"]},"url":{"type":"string","format":"uri"},"title":{"type":"string","nullable":true},"text":{"type":"string","nullable":true},"qualityScore":{"type":"number","minimum":0,"maximum":1,"nullable":true},"qualityFlags":{"type":"array","items":{"type":"string"},"nullable":true},"renderedWithJs":{"type":"boolean","nullable":true,"description":"True si la page a nécessité un rendu JavaScript."},"proposedChunkCount":{"type":"integer","minimum":0,"nullable":true},"errorReason":{"type":"string","nullable":true}}},"PublicScrapeExtractRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL HTTPS publique à extraire (XOR avec pastedText)."},"pastedText":{"type":"string","minLength":40,"maxLength":50000,"description":"Texte collé depuis LinkedIn ou sites protégés (XOR avec url)."},"targetSchema":{"type":"string","enum":["JOB_POSTING","OPPORTUNITY"],"description":"Type de données cible à extraire."},"customFields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"description":{"type":"string"}}},"maxItems":10,"default":[],"description":"Champs additionnels à extraire via LLM."},"followDetailPages":{"type":"boolean","default":false,"description":"Suivre les pages détail des items (défaut false en one-shot)."}},"required":["targetSchema"]},"PublicScrapeExtractJobResponse":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING"]}}},"PublicScrapeExtractResult":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","READY","FAILED"]},"url":{"type":"string","format":"uri","nullable":true},"targetSchema":{"type":"string","enum":["JOB_POSTING","OPPORTUNITY"],"nullable":true},"records":{"type":"array","items":{"type":"object","properties":{"data":{"type":"object"}}},"nullable":true,"description":"Enregistrements JSON extraits."},"stats":{"type":"object","nullable":true,"description":"Statistiques extraction (items trouvés, méthode)."},"creditsCharged":{"type":"integer","minimum":0,"nullable":true,"description":"Nombre de crédits IA consommés (1 crédit par item LLM)."},"errorReason":{"type":"string","nullable":true}}},"CreateScrapeSource":{"type":"object","required":["name","seedUrl","targetSchema"],"properties":{"name":{"type":"string","minLength":2,"maxLength":120},"seedUrl":{"type":"string","format":"uri"},"targetSchema":{"type":"string","enum":["JOB_POSTING","OPPORTUNITY"]},"schedule":{"type":"string","enum":["MANUAL","DAILY","WEEKLY"],"default":"MANUAL"},"customFields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"description":{"type":"string"}}}},"recipe":{"type":"object"},"outputs":{"type":"object"}}},"UpdateScrapeSource":{"type":"object","properties":{"name":{"type":"string"},"recipe":{"type":"object"},"customFields":{"type":"array"},"schedule":{"type":"string","enum":["MANUAL","DAILY","WEEKLY"]},"outputs":{"type":"object"},"status":{"type":"string","enum":["ACTIVE","PAUSED"]}}}}}}