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
| Event | Description |
|---|
checkout.completed | A checkout session was paid successfully |
subscription.created | A new subscription was created after first payment |
subscription.renewed | An existing subscription was renewed for another period |
subscription.canceled | A subscription was canceled (by customer or after grace period) |
Setup
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. 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.
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:
| Header | Description |
|---|
webhook-id | Unique message ID (use for idempotency) |
webhook-timestamp | Unix timestamp (seconds) when the event was sent |
webhook-signature | HMAC-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:
| Attempt | Delay after failure |
|---|
| 1st retry | 5 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 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.