Skip to main content

POST /api/webhooks/payment

Receive payment lifecycle events from the payment provider. Route name: webhooks.payment
Path: /api/webhooks/payment (note: no /v1 prefix — legacy path maintained)
Authentication: HMAC signature verification (see below)
CSRF: Exempt
This endpoint is exempt from CSRF verification. Access control is enforced exclusively through HMAC signature validation on every incoming request.

Signature verification

Every request must include an X-Signature header containing an HMAC-SHA256 signature of the raw request body. The server verifies the signature using the secret configured in PAYMENT_WEBHOOK_SECRET (.env):
PAYMENT_WEBHOOK_SECRET=whsec_your_secret_here
The expected signature is computed as:
hmac_sha256(raw_request_body, PAYMENT_WEBHOOK_SECRET)
If the X-Signature header is missing or does not match, the endpoint returns 403 Forbidden.
# Example: computing the signature for testing
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$PAYMENT_WEBHOOK_SECRET" -hex | awk '{print $2}')

Request format

X-Signature
string
required
HMAC-SHA256 hex signature of the raw request body.
event
string
required
Event type identifier (see supported events below).
data
object
required
Event payload. Structure varies by event type.

Supported events

Event typeDescription
Payment confirmedOrder payment successfully completed
Payment rejectedPayment attempt failed or was declined
Subscription createdNew subscription activated
Subscription cancelledSubscription terminated
Subscription renewedSubscription billing cycle renewed
Event type strings are defined by the payment provider integration. Check the HandlePaymentWebhookAction domain action for the full list of handled event types.

Response

StatusBodyMeaning
200 OK{"status": "ok"}Event processed successfully
202 Accepted{"status": "ignored"}Event received but not processed (unknown type)
403 Forbidden{"message": "Invalid signature"}Signature missing or invalid
422 Unprocessable Entity{"status": "invalid"}Event payload failed domain validation
500 Internal Server Error{"status": "error"}Unexpected processing error

Idempotency

All incoming webhook events are persisted to the webhook_logs table via the WebhookLog model before processing. This provides:
  • Idempotency: Duplicate events with the same external_event_id can be detected by querying the log.
  • Audit trail: Every received payload is stored for forensic debugging.
  • Replay support: Failed events can be replayed from the log.

WebhookLog model

ColumnTypeDescription
idintegerAuto-increment primary key
providerstringPayment provider identifier
external_event_idstringUnique event ID from the provider
event_typestringNormalized event type string
payloadJSONFull raw payload
created_atdatetimeWhen the event was received
Do not delete rows from webhook_logs. This table is the source of truth for payment audit trails and must be retained for compliance purposes.

Example webhook delivery

PAYLOAD='{"event":"payment.confirmed","data":{"transaction_id":"txn_abc123"}}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "whsec_your_secret" -hex | awk '{print $2}')

curl -X POST https://yourdomain.com/api/webhooks/payment \
  -H "Content-Type: application/json" \
  -H "X-Signature: $SIGNATURE" \
  -d "$PAYLOAD"

Build docs developers (and LLMs) love