Dose OS · API publique
Référence des endpoints HTTP exposés par Dose OS — health check, MCP JSON-RPC 2.0 (compatible Claude Desktop), import/export CSV des leads, webhooks signés (DocuSign, Stripe, WhatsApp) et générateur d'OG images. Tous les exemples utilisent curl; les tokens et secrets restent en variables d'environnement (jamais inline).
Conventions
- Base URL ·
https://dose-os.vercel.appen production. Substituer par votre URL custom si self-hosté. - Format · JSON pour les réponses (
application/json) sauf exports CSV (text/csv; charset=utf-8) et OG images (image/png). - Erreurs · forme canonique
{ error: string, ... }. Les webhooks répondent généralement200même en cas d'échec applicatif pour éviter les retries en boucle. - Rate limits· pas de quota agressif côté plateforme. Restez raisonnable (~10 req/s) ; les webhooks externes sont contrôlés par leurs émetteurs (Meta, Stripe, DocuSign).
Health check public utilisé par les sondes externes (UptimeRobot, Vercel) et la page /status interne. Retourne toujours 200 si le process Next.js est up.
Réponse · 200 OK
{
"status": "ok",
"name": "dose-os",
"timestamp": "2026-04-28T09:12:34.000Z",
"version": "abc1234"
}version reflète VERCEL_GIT_COMMIT_SHA tronqué à 7 caractères, ou "dev" en local.
Exemple
curl https://dose-os.vercel.app/api/healthNotes
force-dynamic· pas de cache, le timestamp est toujours frais.- Pas d'auth · usage prévu pour des sondes anonymes.
Génère dynamiquement une image OpenGraph (1200×630) à partir d'un titre et d'un sous-titre fournis en query string. Utilisé par les metadata Next.js des pages publiques.
Query parameters
- title
- string · ≤ 90 caractères · défaut « Votre agence Meta Ads pilotée par l'IA »
- subtitle
- string · ≤ 120 caractères · défaut « Coût/réservation < 15 € · 13 restaurants parisiens »
Réponse · 200 OK
image/png · 1200×630 · Cache-Control: public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400.
Exemple
curl -o og.png \
"https://dose-os.vercel.app/api/og?title=Hello&subtitle=World"Notes
- Edge runtime · génération < 200ms en cold start.
- Charte alignée sur
globals.css· gradient ink → gold. - Une variante statique pleine page existe sur
/opengraph-image.
Exporte la pipeline leads au format CSV (UTF-8 BOM-prefixed pour Excel). Filtres optionnels en query string. Limite stricte à 10 000 lignes par appel.
Query parameters
- status
- enum ·
new | contacted | qualified | won | lost - temperature
- enum ·
cold | warm | hot· dérivé du score (≥70 hot, ≥40 warm, sinon cold) - since
- ISO 8601 · valeur invalide silencieusement ignorée
Schéma CSV
id,name,email,phone,status,score,temperature,source,client_slug,created_at,last_activity_atBOM UTF-8 en tête, séparateur virgule, CRLF en fin. last_activity_at tombe en fallback sur created_at si aucune activité.
Réponses
200·text/csv; charset=utf-8·Content-Disposition: attachment; filename="leads-YYYY-MM-DD.csv"403·{ error: 'agency_only' }si l'utilisateur n'est pas membre agence500·{ error: 'query_failed', details }
Exemple
# Export "hot" leads only since 2026-01-01
curl -L -b cookies.txt \
"https://dose-os.vercel.app/api/leads/export.csv?temperature=hot&since=2026-01-01" \
-o leads.csvNotes
- Auth · session Supabase + appartenance agence (cf.
isAgencyMember()). Pas de redirect 302 vers /login (un download d'HTML serait pire que la 403). - Pas de pagination · cap dur à 10 000 leads. Pour des volumes plus grands, utiliser MCP
list_recent_leads. Cache-Control: no-store.
Importe un CSV de leads (multipart/form-data, champ file). Détection automatique des colonnes, validation Zod par ligne, insertion par batch de 100. Cap à 5 MB.
Request · multipart/form-data
- file
- File · CSV obligatoire · ≤ 5 MB · première ligne = en-têtes
Colonnes reconnues (mapping fuzzy) · name, email, phone, source, stage, score, client_slug. Au moins email ou phonerequis. Si l'agence n'a qu'un client, client_slug est optionnel.
Réponse · 200 OK
{
"ok": 142,
"errors": [
{ "row": 17, "message": "email_invalid" },
{ "row": 23, "message": "missing_contact" },
{ "row": 89, "message": "unknown_client_slug" }
]
}Le statut HTTP reste 200 même en cas d'échecs partiels — le client lit errors[] pour surfacer les problèmes ligne par ligne.
Codes d'erreur
400·file_missingouinvalid_form403·agency_only413·file_too_large(max 5 MB)
Exemple
curl -X POST -b cookies.txt \
-F "file=@leads.csv" \
https://dose-os.vercel.app/api/leads/importNotes
- Idempotence · pas de dédoublonnage automatique côté serveur. Si vous ré-importez le même CSV, vous créerez des doublons. Filtrez en amont si besoin.
- Insertion par batch de 100 lignes — un échec batch remonte sous la première ligne du batch dans
errors[]. - RLS appliquée · les leads sont créés sur le
client_iddérivé duclient_slug.
Endpoint MCP (Model Context Protocol) JSON-RPC 2.0. Compatible Claude Desktop. Expose 8 outils pour piloter l'agence (clients, leads, métriques, approbations, audits).
Authorization header
Authorization: Bearer $DOSE_MCP_TOKENLe token est défini via la variable d'environnement DOSE_MCP_TOKEN. Si la variable est vide, l'endpoint répond 503 mcp_disabled. Comparaison constant-time (anti timing attack).
Méthodes JSON-RPC supportées
initialize· handshake MCPtools/list· catalogue des 8 outilstools/call· exécution d'un outil avec arguments validés Zod
Les requêtes batch (array de payloads) sont supportées selon JSON-RPC 2.0 §6 — les notifications (sans id) sont silencieusement ignorées dans la réponse.
GET /api/mcp · descriptor
{
"enabled": true,
"transport": "json-rpc-2.0",
"methods": ["initialize", "tools/list", "tools/call"]
}Utile pour vérifier que l'URL est joignable depuis Claude Desktop avant de configurer le bearer.
Exemple · tools/list
curl -X POST https://dose-os.vercel.app/api/mcp \
-H "Authorization: Bearer $DOSE_MCP_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'Exemple · tools/call
curl -X POST https://dose-os.vercel.app/api/mcp \
-H "Authorization: Bearer $DOSE_MCP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_client_metrics",
"arguments": { "client_slug": "roger-la-grenouille", "days": 30 }
}
}'Erreurs
401·{ error: 'unauthorized' }(header absent ou token incorrect)503·{ error: 'mcp_disabled' }(la variableDOSE_MCP_TOKENn'est pas définie)400· JSON malformé · réponse JSON-RPCparse_error (-32700)- Erreurs JSON-RPC standards ·
-32601method not found,-32602invalid params,-32603internal error
Catalogue des 8 outils MCP
Le contrat complet (inputSchema JSON Schema) est exposé via tools/list.
list_clientsListe les clients agence actifs (slug, budget mensuel, seuil auto-approve). Param optionnelinclude_inactive.get_client_metricsRéservations, couverts, coût par résa et revenu estimé sur une fenêtre. Paramsclient_slug(req),days1-90 (def 30).list_pending_approvalsDemandes d'approbation non résolues en attente. Param optionnellimit1-200 (def 50).approve_optimizer_actionApprouve une action optimizer (budget_increase / budget_decrease / pause_campaign / duplicate_winner) et l'exécute sur Meta. Paramsactivity_id(uuid, req),note≤500 chars.list_recent_leadsLeads récents tous clients confondus. Params optionnelslimit1-500 (def 50),client_slug.get_agent_runsRuns d'agents récents pour debug cron. Params optionnelsagent,status(queued | running | succeeded | failed),limit1-200 (def 25).trigger_auditDéclenche l'agentaudit_strategiqueen modemanual:on_demand. Paramclient_slug(req). Retourne scanned / findings / cost.get_reservations_30dAgrégat des réservations sur N jours par client (count, covers, cost_per_reservation). Param optionneldays1-90 (def 30).
Webhook DocuSign Connect. Met à jour brand_context.contract sur l'événement envelope_status_change. Vérification HMAC-SHA256 stricte (timing-safe).
Headers attendus
Content-Type: application/json
x-docusign-signature-1: <base64(HMAC_SHA256(rawBody, $DOCUSIGN_WEBHOOK_SECRET))>Payload (subset lu)
{
"event": "envelope-completed",
"data": {
"envelopeId": "abc-123",
"envelopeSummary": {
"status": "completed",
"completedDateTime": "2026-04-28T09:00:00.000Z"
}
}
}L'ancienne forme « flat » (envelopeId + envelopeStatus au top-level) est aussi supportée. Statuts reconnus · sent | delivered | completed | declined | voided | created.
Réponses
200·{ ok: true }200·{ ok: false, reason: 'client_not_found' }(envelope_id inconnu — DocuSign ne retry pas sur 2xx)400·{ error: 'invalid json' }ou{ error: 'missing envelope_id' }401·{ error: 'unauthorized' }(HMAC invalide ou absent)503·{ error: 'webhook disabled' }(la variableDOCUSIGN_WEBHOOK_SECRETn'est pas configurée)
Notes
- Activity
contract_signedémise surcompleted,contract_voidedsurdeclined | voided. - Audit log
contract.status_updatedsystématique. Notification Slack + emailcontract_signedbest-effort sur completion. - Idempotence · le serveur merge l'objet
brand_context.contractsans écrasersigned_at/voided_atexistants.
Webhook Stripe pour le cycle de facturation (checkout.session.completed, invoice.paid, customer.subscription.deleted). Vérification stripe-signature stricte sur le raw body.
Headers attendus
Content-Type: application/json
stripe-signature: t=...,v1=...,v0=...Format standard Stripe · verifyStripeSignature() côté serveur lit le raw body avant tout parsing JSON.
Événements consommés
checkout.session.completed· seedbrand_context.stripe(customer_id + subscription_id), activitysubscription_paidinvoice.paid·last_invoice_paid_at+status: "active". Première facture →subscription_paid, suivantes →subscription_renewedcustomer.subscription.deleted·status: "canceled", activitysubscription_cancelled
Tout autre event.type est silencieusement acquitté (200 ignored) pour éviter les retry storms Stripe.
Stratégie de réconciliation
Le client est résolu via (1) metadata.client_id côté Stripe (préféré, posé à la création de la session) puis (2) fallback sur customer_id stocké dans brand_context.stripe.customer_id. Si aucune correspondance · réponse 200 { ok: false, reason: 'client_not_found' } (Stripe ne retry pas sur 2xx).
Réponses
200·{ ok: true }ou{ ok: true, ignored: '<event.type>' }401· signature invalide400· JSON malformé503·STRIPE_WEBHOOK_SECRETnon défini
Notes
- Runtime Node.js obligatoire ·
request.text()sur le raw body pour la vérification de signature. - Les montants sont reçus en cents et persistés en euros.
- Audit log
stripe.<event.type>systématique avec event_id pour traçabilité.
Webhook WhatsApp Cloud API (Meta). Reçoit les réponses des clients aux demandes d'approbation et exécute la décision (OK / STOP) sur la dernière approval_requested non résolue.
GET · handshake Meta
GET /api/webhooks/whatsapp
?hub.mode=subscribe
&hub.verify_token=$WHATSAPP_VERIFY_TOKEN
&hub.challenge=<random>Retourne le challenge en clair (200 text/plain) si le verify_token matche · sinon 403 forbidden.
POST · payload Meta (subset lu)
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"messages": [{
"from": "33612345678",
"type": "text",
"text": { "body": "OK" }
}]
}
}]
}]
}Détection d'intention
- approve ·
/^(ok|oui|approve|approuv|go|yes)\b/i - deny ·
/^(stop|non|no|refuse|skip)\b/i - sinon · unknown → message d'aide envoyé au sender.
Réponse
Toujours 200 { ok: true } pour éviter les retries Meta. Les échecs internes sont loggés via audit_logs / console.error.
Notes
- Le numéro
fromest matché contreclients.whatsapp_number(avec / sans préfixe « + »). Si aucun client ne matche, le message est ignoré silencieusement. - Sur approve, exécute
executeOptimizerAction()et pose une activityapproval_granted. Sur deny, activityapproval_denied. - Idempotence · seule la dernière
approval_requestednon résolue est ciblée. Le payload est marquéresolved_ataprès décision. - Pas de signature HMAC explicite en POST · Meta s'appuie sur le verify_token + canal dédié. Ne pas exposer cet endpoint sans un
WHATSAPP_VERIFY_TOKENconfiguré.
Endpoint debug agency-only qui envoie un message WhatsApp réel pour valider la configuration WHATSAPP_BUSINESS_TOKEN + WHATSAPP_PHONE_NUMBER_ID. À utiliser après chaque rotation de token Meta.
Query parameters
- to
- E.164 sans «+» (e.g.
33612345678) · prioritaire sur l'env. Quand absent, fallback surWHATSAPP_TEST_TO.
Réponses
200·{ ok: true, to, sent_at }400·missing_to(ni query, ni env)401·unauthorized403·agency_only503·whatsapp_not_configured502·{ ok: false, to, error }(la Graph API a refusé)
Exemple
curl -L -b cookies.txt \
"https://dose-os.vercel.app/api/test/whatsapp/send?to=33612345678"Notes
- Coût · ~0,005 à 0,05 €/message selon la zone (Meta Cloud tarif business). Usage debug exclusif — pas exposé en public.
- Le message envoyé contient un timestamp ISO pour faciliter le matching côté inbox WhatsApp.
Endpoint debug agency-only qui valide Google Calendar Domain-Wide Delegation : tente listCalendars(impersonate) + getNextSlots(60min × 5) sur l'utilisateur impersonné.
Query parameters
- impersonate
- email Workspace org · default
GOOGLE_AGENCY_IMPERSONATE_EMAILougaetan@dosedefrance.com
Réponse · 200
{
"ok": true,
"impersonate": "gaetan@dosedefrance.com",
"calendars_count": 4,
"calendars": [
{ "id": "primary", "summary": "Gaetan", "primary": true,
"timeZone": "Europe/Paris", "accessRole": "owner" }
],
"next_slots_count": 5,
"next_slots": [
{ "start": "2026-04-29T08:00:00.000Z",
"end": "2026-04-29T09:00:00.000Z",
"label": "Mardi 29 avril · 10h00" }
]
}Erreurs
401·unauthorized403·agency_only503·gcal_not_configured502·gcal_dwd_failed(Workspace admin n'a probablement pas autorisé le scope calendar pour le client_id du service account — voiradmin.google.com→ Sécurité > Délégation à l'échelle du domaine).
Notes
getNextSlotscible la fenêtre Lun-Ven 10h-18h Europe/Paris, snap demi-heure, max 1 slot/jour, durée 60 min. Aligné sur la landing /audit-gratuit.- Cache token DwD par {impersonate} (60 min) côté server process — pas de double JWT exchange entre les deux appels parallèles.