All posts

Schedule a Stripe webhook in three lines

Stripe sends webhook events the moment they happen — checkout.session.completed fires at checkout, customer.subscription.created at signup. But a lot of product logic is about the future: remind a user three days before a trial ends, run a deferred charge, or send a re-engagement nudge a week later. Stripe doesn't schedule arbitrary future calls to your own endpoints — so you reach for a cron job, a queue, or a delayed task you have to babysit.

With SendItWhenever you schedule a delayed webhook right from inside your existing Stripe handler, in three lines.

1. Install the SDK

npm install @sendithq/sdk

2. Schedule a follow-up from your Stripe handler

When a checkout completes, schedule the reminder for three days before the trial ends. The payload is encrypted at rest the moment it's stored.

import { SendIt } from "@sendithq/sdk";

const sendit = new SendIt(process.env.SENDIT_API_KEY!);

// inside your Stripe webhook route
if (event.type === "checkout.session.completed") {
  const session = event.data.object;
  const trialEnd = session.subscription_details?.trial_end; // unix seconds
  const fireAt = new Date((trialEnd - 3 * 24 * 60 * 60) * 1000);

  await sendit.schedule({
    url: "https://api.myapp.com/reminders/trial-ending",
    fireAt: fireAt.toISOString(),
    payload: { customerId: session.customer, email: session.customer_email },
  });
}

3. Receive and verify the call

At the scheduled second, SendItWhenever POSTs your payload with an HMAC signature in the X-SendIt-Signature header. verifySignature() reads that header for you — pass the raw request body (parsed JSON breaks the signature) and verify before trusting the request:

import express from "express";

const sendit = new SendIt(process.env.SENDIT_API_KEY!, {
  signingSecret: process.env.SENDIT_SIGNING_SECRET!,
});

// raw body is required — HMAC is computed over the exact bytes
app.post(
  "/reminders/trial-ending",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = sendit.verifySignature({ headers: req.headers, rawBody: req.body });
    if (!ok) return res.status(401).end();

    // …send the "your trial ends soon" email
    res.status(200).end();
  },
);

What you get for free

No broker, no cron server, no IAM. The delivery is signed, the payload is encrypted at rest with AES-256-GCM, and failures retry up to five times with exponential backoff before landing in a Dead Letter Queue — all visible in the dashboard. Firing is second-level precise with Early Firing to offset round-trip latency.

This same pattern covers deferred charges, dunning sequences, delayed onboarding emails, and any "do X later" flow that starts from a Stripe event.

Get started