RevKeen Docs
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

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

const client = new RevKeen({ apiKey: process.env.REVKEEN_API_KEY! });

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 = client.webhooks.verify(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 });
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-RevKeen-Signature")

    event, err := revkeen.Webhooks.Verify(body, sig, os.Getenv("REVKEEN_WEBHOOK_SECRET"))
    if err != nil {
        http.Error(w, "invalid signature", http.StatusBadRequest)
        return
    }

    if event.Type == "payment.succeeded" {
        if alreadyProcessed(event.ID) {
            w.WriteHeader(http.StatusOK)
            return
        }
        payment := event.Data.Object.(*revkeen.Payment)
        fulfillOrder(payment)
        markProcessed(event.ID)
    }

    w.WriteHeader(http.StatusOK)
}
use RevKeen\Webhooks\WebhookVerifier;

$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_REVKEEN_SIGNATURE'] ?? '';
$event = WebhookVerifier::verify($rawBody, $sig, getenv('REVKEEN_WEBHOOK_SECRET'));

if ($event->type === 'payment.succeeded') {
    if (alreadyProcessed($event->id)) {
        http_response_code(200);
        exit;
    }
    $payment = $event->data->object;
    fulfillOrder($payment);
    markProcessed($event->id);
}

http_response_code(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