| name | paystack-webhooks-events |
| description | Implement, debug, or test @alexasomba/better-auth-paystack webhook handling. Use for Paystack webhook signatures, trusted IP checks, webhook.secret/paystackWebhookSecret behavior, charge.success, reconcilePaystackTransaction, subscription.create, subscription.disable, subscription.enable, product quantity updates, subscription status changes, metadata parsing, and event hooks.
|
| type | core |
| library | @alexasomba/better-auth-paystack |
| library_version | 3.0.0 |
| license | MIT |
| compatibility | Node.js >=22.0.0; better-auth ^1.6.9; @alexasomba/paystack-node 1.10.x; @alexasomba/better-auth-paystack >=3.0.0 <4.0.0 |
| sources | ["alexasomba/better-auth-paystack:src/routes.ts","alexasomba/better-auth-paystack:src/types.ts","alexasomba/better-auth-paystack:src/utils.ts","alexasomba/better-auth-paystack:test/paystack.test.ts","alexasomba/better-auth-paystack:test/seat_billing.test.ts"] |
Webhook Contract
The Better Auth endpoint is registered as auth.api.paystackWebhook and mounted under
/api/auth/paystack/webhook by the plugin. Always send the raw JSON body that Paystack signed.
Signature verification uses HMAC SHA-512 over the raw request body. Secret precedence is:
webhook.secret
paystackWebhookSecret
secretKey
Prefer webhook.secret in new code. Keep paystackWebhookSecret only for compatibility.
Core Patterns
Verify before processing
Webhook code must reject invalid signatures before parsing business effects:
const signature = createHmac("sha512", webhookSecret).update(payload).digest("hex");
If webhook.verifyIP is true, the request must come from webhook.trustedIPs or the built-in
Paystack IP allowlist. Preserve support for common forwarded IP headers when changing this path.
Treat charge.success as reconciliation
charge.success can update multiple local records:
- mark a pending transaction as
success
- finalize local subscription checkout when metadata identifies a plan
- apply checkout-based proration metadata with
type: "proration"
- capture
authorization.authorization_code for local renewals
- decrement one-time product quantity when product metadata is present
Do not grant paid access from a redirect alone. The callback should verify the transaction and
webhooks should reconcile persisted state.
For trusted server paths that need to re-verify and apply the plugin's local side effects outside a browser session, call reconcilePaystackTransaction:
import { reconcilePaystackTransaction } from "@alexasomba/better-auth-paystack";
await reconcilePaystackTransaction(ctx, paystackOptions, {
reference,
source: "webhook",
referenceId,
});
The helper is suitable for webhook handlers, queue retries, cron jobs, and admin repair actions.
Handle subscription events idempotently
Paystack subscription events should update matching subscriptions without assuming one delivery:
subscription.create: activate or update the subscription and call creation hooks
subscription.disable: mark cancellation/non-renewal and call cancel hooks
subscription.enable: restore active state when Paystack re-enables a subscription
Match by known Paystack identifiers first, then metadata such as referenceId and plan when needed.
Avoid creating duplicate subscriptions on repeated webhook delivery.
Common Mistakes
Parsing body before signature verification
Do not route Paystack webhooks through a generic JSON handler that loses the exact signed payload.
The signature must be checked against the same raw string Paystack sent.
Assuming all metadata is an object
Paystack metadata may arrive as an object or a JSON string. Existing route code handles both forms.
Keep that tolerance when changing metadata handling.
Forgetting product side effects
Webhook work is not subscription-only. Successful product purchases must update transaction state and
respect product quantity/unlimited settings.
Verification
Run focused tests after webhook changes:
vp test test/paystack.test.ts
vp test test/seat_billing.test.ts
Also run vp check before landing broad route or type changes.