Aller au contenu
Tous les articles
templatesstripenotioncloudflare-workersautomation

Webhook Stripe ↔ Notion en 50 lignes

Code source complet d'un Cloudflare Worker qui crée une fiche Notion à chaque paiement Stripe + email Calendly auto.

BastienBastien2 avril 20265 min
Article Devoria

Le besoin

Une agence de formation cliente nous a demandé : "Quand un élève paie via Stripe, je veux automatiquement une fiche Notion créée dans la base 'Élèves', un email envoyé avec un lien Calendly, et l'élève ajouté à la mailing list Klaviyo. Manuellement, ça nous prend 10 minutes par client."

Volume : 30-50 paiements / mois. Donc 5-8 heures de manipulation manuelle par mois, à 35 €/h, ça fait 200-300 €/mois "gaspillés".

On a livré la solution en 1 jour. Voici le code et les pièges qu'on a évités.

L'architecture

Stripe ──webhook──> Cloudflare Worker ──> Notion API
                                     ──> Resend (email Calendly)
                                     ──> Klaviyo API (subscribe)

Pourquoi Cloudflare Workers et pas Vercel/Lambda :

  • Cold start sub-50ms (Stripe attend < 5 s, sinon retry).
  • Idempotency naturelle via KV.
  • Coût : 5 $/mois pour 10 M req. Gratuit en dessous de 100 k req/mois.

Le code complet (50 lignes)

// worker.ts
import Stripe from "stripe";
import { Client as NotionClient } from "@notionhq/client";

interface Env {
  STRIPE_KEY: string;
  STRIPE_WEBHOOK_SECRET: string;
  NOTION_KEY: string;
  NOTION_DB_ID: string;
  RESEND_KEY: string;
  KLAVIYO_KEY: string;
  EVENTS_KV: KVNamespace; // Idempotency
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const sig = req.headers.get("stripe-signature");
    const body = await req.text();
    const stripe = new Stripe(env.STRIPE_KEY);

    // 1. Vérification signature
    const event = await stripe.webhooks.constructEventAsync(
      body,
      sig!,
      env.STRIPE_WEBHOOK_SECRET,
    );

    // 2. Idempotency : on stocke l'event.id dans KV pour dédupe
    const seen = await env.EVENTS_KV.get(event.id);
    if (seen) return new Response("ok (dupe)", { status: 200 });
    await env.EVENTS_KV.put(event.id, "1", { expirationTtl: 86400 });

    // 3. On ne traite que checkout.session.completed
    if (event.type !== "checkout.session.completed") {
      return new Response("ok (ignored)", { status: 200 });
    }

    const session = event.data.object as Stripe.Checkout.Session;
    const email = session.customer_details?.email;
    const name = session.customer_details?.name;
    if (!email) return new Response("ok (no email)", { status: 200 });

    // 4. Création Notion + email Calendly + Klaviyo en parallèle
    await Promise.all([
      createNotionEntry(env, { email, name, sessionId: session.id }),
      sendCalendlyEmail(env, { email, name }),
      addToKlaviyo(env, { email, name }),
    ]);

    return new Response("ok", { status: 200 });
  },
};

Les 3 helpers (15 lignes)

async function createNotionEntry(env: Env, p: { email; name; sessionId }) {
  const notion = new NotionClient({ auth: env.NOTION_KEY });
  await notion.pages.create({
    parent: { database_id: env.NOTION_DB_ID },
    properties: {
      Name: { title: [{ text: { content: p.name ?? p.email } }] },
      Email: { email: p.email },
      Stripe: { url: `https://dashboard.stripe.com/payments/${p.sessionId}` },
      Status: { select: { name: "À onboarder" } },
    },
  });
}

async function sendCalendlyEmail(env: Env, p: { email; name }) {
  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${env.RESEND_KEY}` },
    body: JSON.stringify({
      from: "[email protected]",
      to: p.email,
      subject: "Bienvenue ! Réservez votre session de cadrage",
      html: `Bonjour ${p.name ?? ""}, réservez votre session : <a href="https://cal.com/ecole/onboarding">Calendly</a>`,
    }),
  });
}

async function addToKlaviyo(env: Env, p: { email; name }) {
  await fetch("https://a.klaviyo.com/api/profiles/", {
    method: "POST",
    headers: {
      Authorization: `Klaviyo-API-Key ${env.KLAVIYO_KEY}`,
      "Content-Type": "application/json",
      revision: "2024-10-15",
    },
    body: JSON.stringify({
      data: {
        type: "profile",
        attributes: { email: p.email, first_name: p.name },
      },
    }),
  });
}

50 lignes (tout compris). Déployable immédiatement sur Cloudflare Workers.

Les 3 pièges qu'on a évités

1. Webhook double-fire

Stripe peut envoyer le même event 2-3 fois (réseau, retry). Sans déduplication, vous créez 3 fiches Notion. Le KV avec event.id règle ça en 2 lignes.

2. Timeout Stripe

Stripe attend 5 secondes max pour le 200. Sur un cold start AWS Lambda, c'était souvent 6-7 secondes. D'où Cloudflare Workers qui démarre instantanément.

3. Échec partiel

Si Notion crashe mais Klaviyo passe, vous voulez retry uniquement Notion. Solution : chaque sous-task est dans un try/catch, et les échecs sont loggés dans une queue (Cloudflare Queues) pour retry async. Pas critique pour ce client (volume faible), mais on l'a documenté pour la V2.

Variantes possibles

  • Stripe → Airtable au lieu de Notion : changer juste le helper.
  • Stripe → Slack pour notif équipe : ajouter un 4e helper, 8 lignes.
  • Stripe → HubSpot pour lead score : remplacer Klaviyo.

Le pattern reste identique : Worker reçoit, dédupe, dispatch vers N outils en parallèle.

Le coût total

  • Cloudflare Workers : 0 € (sous le free tier)
  • KV : 0 € (sous le free tier)
  • Resend : 0 € (3 000 emails/mois gratuits)

Total : 0 €/mois. Pour automatiser 5-8 heures de manuel.

Si vous avez un workflow similaire à automatiser (Stripe vers n'importe quel outil), parlez-nous. C'est typiquement le genre de mission qu'on livre en 1 jour, code source dans votre repo.

Ajoutez un développeur à votre équipe, au forfait

1. Briefez (simplement)2. Recevez (rapidement)3. Recommencez (à volonté)
+35

+ de 35 marques
nous font confiance