Prerequisites

Step 1: Create a Webhook Endpoint

From the Dashboard

  1. Go to your organization Settings > Webhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL (e.g., https://your-app.com/api/webhooks/macropay)
  4. Select the events you want to receive
  5. Click Create and copy the signing secret (whsec_...)

From the API

curl -X POST https://api.macropay.com/v1/webhooks/endpoints \
  -H "Authorization: Bearer $MACROPAY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/api/webhooks/macropay",
    "events": [
      "order.paid",
      "subscription.created",
      "subscription.active",
      "subscription.canceled",
      "customer.created"
    ],
    "format": "raw"
  }'

Step 2: Configure Events

Choose which events to subscribe to based on your use case:
EventDescription
order.paidA one-time or recurring payment was completed
order.refundedAn order was refunded
subscription.createdA new subscription was created
subscription.activeA subscription became active
subscription.canceledA subscription was canceled
subscription.revokedA subscription was revoked (e.g., failed payment)
checkout.createdA checkout session was started
checkout.updatedA checkout session status changed
customer.createdA new customer was created
customer.state_changedA customer’s active subscriptions or entitlements changed
See the full list of events in the Webhook Events reference.

Step 3: Implement Your Handler

Next.js (App Router)

// src/app/api/webhooks/macropay/route.ts
import { Webhooks } from "@macropay/nextjs";

export const POST = Webhooks({
  webhookSecret: process.env.MACROPAY_WEBHOOK_SECRET!,
  onOrderPaid: async (order) => {
    // Provision access for the customer
    await db.users.update({
      where: { email: order.customer.email },
      data: { plan: order.product.name, active: true },
    });
  },
  onSubscriptionCanceled: async (subscription) => {
    // Revoke access
    await db.users.update({
      where: { id: subscription.customer.id },
      data: { active: false },
    });
  },
});

FastAPI (Python)

from fastapi import FastAPI, Request, HTTPException
from standardwebhooks.webhooks import Webhook
import os

app = FastAPI()

@app.post("/api/webhooks/macropay")
async def handle_webhook(request: Request):
    body = await request.body()
    headers = {
        "webhook-id": request.headers.get("webhook-id"),
        "webhook-timestamp": request.headers.get("webhook-timestamp"),
        "webhook-signature": request.headers.get("webhook-signature"),
    }

    wh = Webhook(os.environ["MACROPAY_WEBHOOK_SECRET"])

    try:
        payload = wh.verify(body.decode("utf-8"), headers)
    except Exception:
        raise HTTPException(status_code=401, detail="Invalid signature")

    match payload["type"]:
        case "order.paid":
            order = payload["data"]
            # Provision access
        case "subscription.canceled":
            subscription = payload["data"]
            # Revoke access

    return {"status": "ok"}

Express.js

import express from "express";
import { Webhook } from "standardwebhooks";

const app = express();

app.post(
  "/api/webhooks/macropay",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const body = req.body.toString();
    const headers = {
      "webhook-id": req.headers["webhook-id"] as string,
      "webhook-timestamp": req.headers["webhook-timestamp"] as string,
      "webhook-signature": req.headers["webhook-signature"] as string,
    };

    const wh = new Webhook(process.env.MACROPAY_WEBHOOK_SECRET!);

    try {
      const payload = wh.verify(body, headers);
      // Handle the event based on payload.type
      res.status(200).send("OK");
    } catch {
      res.status(401).send("Invalid signature");
    }
  }
);

Step 4: Test with Sandbox

Using ngrok for local development

ngrok http 3000
Copy the ngrok URL and add it as your webhook endpoint URL in the dashboard (e.g., https://abc123.ngrok-free.app/api/webhooks/macropay).

Triggering a test event

  1. Create a checkout session pointing to one of your products
  2. Complete the checkout using test card 4242 4242 4242 4242
  3. Verify the webhook event arrives at your endpoint
You can also test webhooks locally using the Macropay CLI. See Local Webhook Testing for details.

Step 5: Handle Common Patterns

Idempotency

Macropay may deliver the same event more than once. Use the webhook-id header to deduplicate:
const webhookId = req.headers.get("webhook-id");

// Check if we've already processed this event
const existing = await db.processedWebhooks.findUnique({
  where: { webhookId },
});

if (existing) {
  return new Response("Already processed", { status: 200 });
}

// Process the event, then mark as handled
await db.processedWebhooks.create({ data: { webhookId } });

Async processing

Return a 200 response immediately, then process the event asynchronously to avoid timeouts:
export async function POST(req: Request) {
  // Verify signature first
  const payload = verifyWebhook(req);

  // Queue for async processing
  await queue.publish("macropay-webhook", payload);

  return new Response("OK", { status: 200 });
}
Always return a 2xx status code within 30 seconds. Macropay retries failed deliveries with exponential backoff. See Webhook Delivery for retry details.

What’s Next?

Webhook Events

Full reference of all webhook event types

Signature Verification

Deep dive into signature verification

Webhook Delivery

Retry behavior and delivery guarantees

Customer State

Query customer entitlements directly