Mintcash
ConceptsWebhooks

Webhooks overview

How MintCash notifies your server when state changes. Delivery semantics, retry policy, and what your endpoint needs to handle.

Webhooks are how MintCash tells you that something changed asynchronously — a payment succeeded, a subscription renewed, a refund cleared. Your endpoint receives an HMAC-signed POST; you verify the signature, dedupe the event, and act on it.

The contract

  • Transport: HTTPS POST to the callbackUrl you pass on POST /payments or POST /subscriptions. The first time we see a URL it's registered as a webhook endpoint for your merchant; payments created with a callbackUrl deliver there, and payments created without one fall back to your most recently registered endpoint.
  • Body: JSON, with the same envelope shape across all events
  • Signature: HMAC-SHA256 in the x-signature header, computed over the raw request body using a secret unique to your merchant.
  • At-least-once delivery: we retry on non-2xx until we give up (see below). Your handler must be idempotent.
  • Order: not guaranteed. A succeeded webhook can arrive before a pending one. Handle out-of-order events defensively.

What you must do

Rendering diagram…

In code form:

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-signature");

  if (!verifyWebhook(body, signature, signingSecret)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);

  if (event.environment !== process.env.MINTCASH_ENV) {
    return new Response("wrong environment", { status: 400 });
  }

  if (await alreadyProcessed(event.eventId)) {
    return new Response("ok", { status: 200 });
  }

  await handleEvent(event);
  await markProcessed(event.eventId);

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

Retry policy

When your endpoint returns non-2xx (or the request errors), MintCash retries the delivery up to 5 times — 6 attempts in total — with exponential backoff (the spacing widens with each attempt). Once the retries are exhausted the event is marked failed. The eventId stays the same across every redelivery of an event, so your dedupe key holds.

Latency budget

Aim to respond inside 10 seconds. A delivery that hangs ties up the attempt until our delivery infrastructure kills it, and it's then retried like any other failure.

If your handler does heavy work (e.g. sending emails, calling other APIs, rebuilding caches), defer that to a background job and return 200 immediately after persisting the event.

Idempotency is non-negotiable

Even if your endpoint is fast and reliable, you'll still see duplicate events occasionally — a transient network blip on our side, a retry that crossed paths with the original, a manual replay. Dedupe by eventId or you'll double-fulfil orders.

Where to go next