Skip to content

Webhooks

Webhooks are how Ridley tells your backend that something happened. Treat them as the source of truth. The browser redirect is for user experience only.

Configure the endpoint per Source in the dashboard (Settings → Sources → Webhook URL). Each Source has its own whsec_ signing secret.

Event Fires when
payment.completed Payment captured successfully
payment.authorized Funds held (manual capture)
payment.captured A held payment was captured
payment.failed Authorization or capture failed
payment.refunded Payment refunded (full or partial)
payment.voided A hold was released
recurring.charged Subscription charged successfully
recurring.failed Subscription charge failed
recurring.suspended Subscription suspended after max retries
recurring.cancelled Subscription cancelled
recurring.paused / recurring.resumed Subscription paused / resumed
recurring.updated Subscription changed
recurring.payment_method_updated Card on file replaced
invoice.created / invoice.sent / invoice.paid / invoice.overdue Invoice lifecycle
POST /your/webhook/url
Content-Type: application/json
X-Webhook-Signature: t=1735689600,v1=5257a8...
X-Webhook-Event: payment.completed
X-Webhook-Event-Id: 550e8400-e29b-41d4-a716-446655440000
{
"event": "payment.completed",
"data": {
"payment_id": 42,
"reference": "order-12345",
"amount": 150.0,
"currency": "JMD",
"status": "PAID",
"card_last": "4242",
"card_brand": "visa",
"metadata": {}
}
}

The signature is an HMAC-SHA256 of "{timestamp}.{raw_request_body}" using your Source’s whsec_ secret. Compute it over the raw body and compare in constant time.

$parts = collect(explode(',', $request->header('X-Webhook-Signature')))
->mapWithKeys(function ($part) {
[$k, $v] = explode('=', $part, 2);
return [$k => $v];
});
$expected = hash_hmac(
'sha256',
$parts['t'].'.'.$request->getContent(),
$source->webhook_secret,
);
if (! hash_equals($expected, $parts['v1'])) {
abort(400, 'Invalid signature');
}
  • Respond with a 2xx quickly and do heavy work asynchronously.
  • Non-2xx responses are retried 3 times with increasing backoff.
  • Every attempt is logged (status, response body, timestamp) for debugging.
  • Endpoints must be public; private/reserved addresses are blocked.

Each delivery is stored against the payment so you can inspect failures in the dashboard. Use the recorded event_id to correlate with your own logs.