Skip to main content
Webhooks let your application receive real-time HTTP notifications when events happen in your moneydevkit account — like a checkout completing or a subscription renewing. Instead of polling the API, you register an endpoint URL and moneydevkit pushes events to you as they occur.

Event Types

EventDescription
checkout.completedA checkout session was paid successfully
subscription.createdA new subscription was created after first payment
subscription.renewedAn existing subscription was renewed for another period
subscription.canceledA subscription was canceled (by customer or after grace period)

Setup

1

Create a webhook endpoint

In your dashboard, navigate to Apps → your app → Webhooks → Add Endpoint.Enter the URL where you want to receive events (e.g. https://yourapp.com/api/webhooks/mdk), then select the event types you want to subscribe to.
2

Store the signing secret

After creating the endpoint, copy the signing secret (starts with whsec_). Store it in your environment variables and install the standardwebhooks package for signature verification:
MDK_WEBHOOK_SECRET=whsec_your_secret_here
npm install standardwebhooks
Keep your signing secret private. Never commit it to source control or expose it in client-side code.
3

Create a webhook handler

Add an endpoint in your app to receive and verify webhook events. Here’s an example using Next.js App Router:
// app/api/webhooks/mdk/route.ts
import { NextRequest, NextResponse } from "next/server"
import { Webhook } from "standardwebhooks"

const secret = process.env.MDK_WEBHOOK_SECRET!

export async function POST(req: NextRequest) {
  const body = await req.text()
  const headers = {
    "webhook-id": req.headers.get("webhook-id") ?? "",
    "webhook-timestamp": req.headers.get("webhook-timestamp") ?? "",
    "webhook-signature": req.headers.get("webhook-signature") ?? "",
  }

  // Verify the signature
  const wh = new Webhook(secret)
  let payload: Record<string, unknown>
  try {
    payload = wh.verify(body, headers) as Record<string, unknown>
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 })
  }

  // Event-specific fields are in payload.data
  const { type, data } = payload as { type: string, data: Record<string, unknown> }

  switch (type) {
    case "checkout.completed":
      // Fulfill the order — data.amountSats, data.customer, etc.
      break
    case "subscription.created":
      // Activate the subscription — data.customerId, data.productId, etc.
      break
    case "subscription.renewed":
      // Extend access for the new period
      break
    case "subscription.canceled":
      // Revoke access or schedule revocation
      break
  }

  return NextResponse.json({ received: true })
}

Event Payloads

Every webhook payload is wrapped in a standard envelope with type, timestamp, id, and a data object containing the event-specific fields.

checkout.completed

Sent when a checkout session is paid successfully.
{
  "type": "checkout.completed",
  "timestamp": "2025-07-01T12:00:00.000Z",
  "id": "evt_abc123",
  "data": {
    "status": "COMPLETED",
    "amountSats": 10000,
    "currency": "SAT",
    "netAmount": 9900,
    "metadata": {},
    "customer": {
      "email": "[email protected]",
      "name": "Alice"
    },
    "product": {
      "id": "prod_xyz",
      "name": "Premium Plan"
    }
  }
}

subscription.created

Sent when a customer’s first subscription payment is confirmed.
{
  "type": "subscription.created",
  "timestamp": "2025-07-01T12:00:00.000Z",
  "id": "evt_def456",
  "data": {
    "status": "active",
    "customerId": "cus_abc",
    "productId": "prod_xyz",
    "amount": 10000,
    "currency": "SAT",
    "recurringInterval": "MONTH",
    "currentPeriodEnd": "2025-08-01T00:00:00.000Z",
    "customer": {
      "email": "[email protected]",
      "name": "Alice"
    }
  }
}

subscription.renewed

Sent when a subscription is successfully renewed. Same shape as subscription.created.
{
  "type": "subscription.renewed",
  "timestamp": "2025-08-01T12:00:00.000Z",
  "id": "evt_ghi789",
  "data": {
    "status": "active",
    "customerId": "cus_abc",
    "productId": "prod_xyz",
    "amount": 10000,
    "currency": "SAT",
    "recurringInterval": "MONTH",
    "currentPeriodEnd": "2025-09-01T00:00:00.000Z",
    "customer": {
      "email": "[email protected]",
      "name": "Alice"
    }
  }
}

subscription.canceled

Sent when a subscription is canceled.
{
  "type": "subscription.canceled",
  "timestamp": "2025-07-15T12:00:00.000Z",
  "id": "evt_jkl012",
  "data": {
    "status": "canceled",
    "customerId": "cus_abc",
    "productId": "prod_xyz",
    "cancelAtPeriodEnd": true,
    "canceledAt": "2025-07-15T12:00:00.000Z",
    "endsAt": "2025-08-01T00:00:00.000Z",
    "customer": {
      "email": "[email protected]",
      "name": "Alice"
    }
  }
}

Signature Verification

moneydevkit signs every webhook using the Standard Webhooks specification. Each request includes three headers:
HeaderDescription
webhook-idUnique message ID (use for idempotency)
webhook-timestampUnix timestamp (seconds) when the event was sent
webhook-signatureHMAC-SHA256 signature of the payload
The standardwebhooks library (used in the handler above) validates the signature, checks that the timestamp is recent (rejecting replay attacks), and returns the parsed payload.

Retry Behavior

If your endpoint returns a non-2xx status code or the request times out, moneydevkit retries with increasing delays:
AttemptDelay after failure
1st retry5 seconds
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry8 hours
After 5 consecutive failures across any events, the endpoint is automatically disabled. You can re-enable it from the dashboard after fixing the issue.

Managing Endpoints

From the dashboard (Apps → your app → Webhooks) you can:
  • Toggle endpoints on/off without deleting them
  • Rotate the signing secret if it’s been compromised
  • Delete endpoints you no longer need
Respond to webhooks with a 2xx status code as quickly as possible. Do any heavy processing asynchronously after acknowledging receipt. moneydevkit times out after 30 seconds — if your handler takes longer, the delivery will be retried.