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
- Verify the signature (always).
- Deduplicate by
event.id. - If this payment settles an invoice → look up the invoice, confirm its state is
paid, run your fulfilment flow. - If this payment is standalone → grant the benefit / deliver the digital good / mark the order paid.
- Return
2xxwithin 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_idis populated. Standalone charges (one-time payments, payment links) have no invoice. Check for null. - Do not read
amount— readamount_minor. All money in RevKeen is in minor units (cents, pence, öre). Human-readable amounts are derived on display.
Related events
payment.failed— use for dunning entryinvoice.paid— recurring billing equivalentrefund.created— reverse of this event