WebhooksEvents

payment.succeeded

Fires when a gateway confirms a successful capture

Fires when the gateway confirms a successful capture. This is the canonical "money moved" event.

For recurring billing, payment.succeeded is paired with invoice.paid — the payment succeeded and the invoice it settles was marked paid. Deduplicate by invoice.id if you trigger fulfilment from both.

Payload

{
  "id": "evt_1a2b3c4d5e6f",
  "type": "payment.succeeded",
  "created": 1705689600,
  "livemode": true,
  "data": {
    "object": {
      "id": "pay_01HK4X7Z2M5N8P0Q3R6S9T2V5",
      "object": "payment",
      "amount_minor": 2499,
      "currency": "USD",
      "status": "succeeded",
      "customer_id": "cus_01HK4X7Z2M5N8P0Q3R6S9T2V5",
      "invoice_id": "inv_01HK4X7Z2M5N8P0Q3R6S9T2V5",
      "payment_method_id": "pm_01HK4X7Z2M5N8P0Q3R6S9T2V5",
      "gateway_transaction_id": "ch_3Nk7mL2eZvKYlo2C1abc",
      "gateway": "nmi",
      "captured_at": "2026-01-19T12:00:00Z",
      "metadata": {
        "order_id": "ord_12345"
      }
    }
  }
}

Handler contract

  1. Verify the signature (always).
  2. Deduplicate by event.id.
  3. If this payment settles an invoice → look up the invoice, confirm its state is paid, run your fulfilment flow.
  4. If this payment is standalone → grant the benefit / deliver the digital good / mark the order paid.
  5. Return 2xx within 30 seconds; push anything slower to a queue.

Handler example

The Go and PHP tabs show generated source shapes only. Their supported package channels are not live; use the available TypeScript SDK today.

import { constructEvent } from "@revkeen/sdk";

export async function handleWebhook(request: Request) {
  const rawBody = await request.text();
  const sig = request.headers.get("x-revkeen-signature")!;
  const secret = process.env.REVKEEN_WEBHOOK_SECRET!;

  const event = constructEvent(rawBody, sig, secret);

  if (event.type === "payment.succeeded") {
    const payment = event.data.object;
    if (await alreadyProcessed(event.id)) return new Response(null, { status: 200 });

    await fulfillOrder({
      paymentId: payment.id,
      amountMinor: payment.amount_minor,
      currency: payment.currency,
      orderId: payment.metadata.order_id,
    });

    await markProcessed(event.id);
  }

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

Common gotchas

  • Do not grant the benefit before verifying. An attacker can POST a fake payment.succeeded — signature verification is the only thing that makes this event trustworthy.
  • Do not assume invoice_id is populated. Standalone charges (one-time payments, payment links) have no invoice. Check for null.
  • Do not read amount — read amount_minor. All money in RevKeen is in minor units (cents, pence, öre). Human-readable amounts are derived on display.

On this page