Skip to content

feat: add Paystack payment integration#28737

Open
MarvelNwachukwu wants to merge 4 commits intocalcom:mainfrom
MarvelNwachukwu:feat/paystack-upstream
Open

feat: add Paystack payment integration#28737
MarvelNwachukwu wants to merge 4 commits intocalcom:mainfrom
MarvelNwachukwu:feat/paystack-upstream

Conversation

@MarvelNwachukwu
Copy link
Copy Markdown

@MarvelNwachukwu MarvelNwachukwu commented Apr 4, 2026

Summary

Adds Paystack as a payment provider for Cal.com. Paystack is Africa's leading payment gateway, processing payments for 200,000+ businesses across Nigeria, Ghana, South Africa, Kenya, and more.

Why: Cal.com currently supports Stripe, PayPal, and a few crypto payment options — none of which serve the African market well. Paystack fills this gap by providing a payment option that supports local currencies (NGN, GHS, ZAR, KES, USD) and local payment methods (bank transfers, mobile money, USSD) that African users expect.

What's included

  • PaymentService — Implements IAbstractPaymentService with create, refund, update, and deletePayment methods
  • PaystackClient — REST API wrapper for Paystack (initialize transaction, verify, refund)
  • Webhook endpoint — Receives Paystack events with HMAC-SHA512 signature verification for secure payment confirmation
  • Verify endpoint — Callback-based payment verification (Paystack redirects here after payment)
  • Event type configuration UI — App card interface for setting price, currency, refund policy, and payment option
  • Setup page — Authenticated form for entering Paystack API keys (public key + secret key)
  • Payment component — Uses Paystack's inline popup checkout on the /payment/[uid] page
  • App install flow — Uses isOAuth: true pattern (same as HitPay) to route through the accounts step → add.ts → setup page

Architecture

Follows the established Cal.com payment app patterns used by HitPay and Stripe:

packages/app-store/paystack/
├── _metadata.ts                          # App metadata
├── config.json                           # App store config
├── zod.ts                                # Zod schemas (appDataSchema, appKeysSchema)
├── api/
│   ├── add.ts                            # Install handler → redirects to setup
│   ├── verify.ts                         # Payment callback verification
│   └── webhook.ts                        # Webhook with signature verification
├── components/
│   ├── EventTypeAppCardInterface.tsx      # Event type app card
│   ├── EventTypeAppSettingsInterface.tsx  # Price/currency/refund settings
│   └── PaystackPaymentComponent.tsx       # Inline checkout popup
├── lib/
│   ├── PaymentService.ts                 # IAbstractPaymentService implementation
│   ├── PaystackClient.ts                 # Paystack REST API client
│   ├── verifyWebhookSignature.ts         # HMAC-SHA512 verification
│   ├── currencyOptions.ts                # Supported currencies
│   └── __tests__/                        # Unit tests
└── pages/setup/
    └── _getServerSideProps.ts            # Authenticated setup page props

Payment flow

  1. Host installs Paystack app → enters API keys on setup page
  2. Host enables Paystack on event type → sets price, currency, refund policy
  3. Attendee books event → clicks "Pay to book"
  4. Booking created with PENDING status → redirect to /payment/[uid]
  5. Paystack inline popup opens → attendee completes payment
  6. Paystack sends webhook → signature verified → booking confirmed
  7. Alternatively: Paystack redirects to /api/integrations/paystack/verify → payment verified → booking confirmed

Test plan

  • Install Paystack from /apps/paystack → verify setup page appears
  • Enter Paystack test API keys → verify they save correctly
  • Configure event type with Paystack payment (price, currency) → verify app card shows
  • Book event as attendee → verify "Pay to book" button and price display
  • Complete payment via Paystack popup → verify booking is confirmed
  • Test refund flow from bookings page
  • Test webhook endpoint with Paystack test events

Screenshots

CleanShot 2026-04-04 at 21 55 44 CleanShot 2026-04-04 at 21 55 55 CleanShot 2026-04-04 at 21 56 35 CleanShot 2026-04-04 at 21 57 08 CleanShot 2026-04-04 at 22 40 40 CleanShot 2026-04-04 at 22 41 02 CleanShot 2026-04-04 at 22 41 06 CleanShot 2026-04-04 at 22 41 13

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 4, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 24 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/app-store/paystack/api/webhook.ts">

<violation number="1" location="packages/app-store/paystack/api/webhook.ts:112">
P1: Webhook idempotency check is non-atomic, allowing concurrent duplicate events to both process payment success and trigger duplicate side effects.</violation>
</file>

<file name="packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx">

<violation number="1" location="packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx:146">
P2: Clearing the number input stores NaN in refundDaysCount because parseInt("") is NaN; this can propagate into saved app data and the controlled value prop.</violation>
</file>

<file name="packages/app-store/paystack/components/PaystackPaymentComponent.tsx">

<violation number="1" location="packages/app-store/paystack/components/PaystackPaymentComponent.tsx:43">
P2: Paystack amount display incorrectly divides by 100 for all currencies, causing 100× under-display for zero-decimal currencies (e.g., XOF/RWF).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/app-store/paystack/api/add.ts">

<violation number="1" location="packages/app-store/paystack/api/add.ts:15">
P2: Invalid `teamId` query values are coerced to `NaN` and treated as falsy, bypassing team-admin validation and silently falling back to user-scoped install instead of returning a validation error.</violation>

<violation number="2" location="packages/app-store/paystack/api/add.ts:22">
P2: Non-atomic `findFirst`→`create` install flow can create duplicate credentials under concurrent requests.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Add Paystack as a payment provider for Cal.com, enabling hosts to collect
payments via Paystack when attendees book events. Paystack is widely used
across Africa and supports NGN, GHS, ZAR, KES, and USD currencies.

Key components:
- PaymentService implementing IAbstractPaymentService (create, refund, update)
- PaystackClient REST wrapper for Paystack API (initialize, verify, refund)
- Webhook endpoint with HMAC-SHA512 signature verification
- Verify endpoint for callback-based payment confirmation
- Event type app card UI for configuring price, currency, and refund policy
- Setup page for entering Paystack API keys (public + secret)
- PaystackPaymentComponent using Paystack inline popup checkout
- App install flow via add.ts with isOAuth: true for proper setup routing

Follows existing payment app patterns (HitPay, Stripe) for:
- App store registration (config.json, _metadata.ts, zod schemas)
- Server-side props for setup page authentication
- Generated file registration (payment.services, apps.metadata, etc.)
- Seed script entry for local development
@MarvelNwachukwu MarvelNwachukwu force-pushed the feat/paystack-upstream branch from 420431f to cf5507f Compare April 4, 2026 21:45
@MarvelNwachukwu MarvelNwachukwu changed the title feat: Add Paystack payment integration feat: add Paystack payment integration Apr 4, 2026
- Webhook: atomic idempotency via updateMany WHERE success=false
- Webhook: rollback success flag if Paystack API verification fails
- EventTypeSettings: guard parseInt("") returning NaN on cleared input
- PaymentComponent: use convertFromSmallestToPresentableCurrencyUnit for zero-decimal currencies
- Add handler: validate teamId is numeric before access check
- Add handler: consolidate owner filter, return 409 for duplicate installs
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/app-store/paystack/api/webhook.ts">

<violation number="1" location="packages/app-store/paystack/api/webhook.ts:114">
P1: Idempotency lock conflates processing lock with final success state, causing unrecoverable stuck payments when exceptions occur after setting `success: true`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Wraps verification + handlePaymentSuccess in try/catch that resets
success=false on failure, so webhook retries can re-process the payment
instead of it being stuck as "already processed".
@MarvelNwachukwu MarvelNwachukwu marked this pull request as ready for review April 4, 2026 22:29
@MarvelNwachukwu MarvelNwachukwu requested a review from a team as a code owner April 4, 2026 22:29
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/app-store/paystack/api/webhook.ts">

<violation number="1" location="packages/app-store/paystack/api/webhook.ts:145">
P1: New rollback catch resets `payment.success` even after successful processing (because `handlePaymentSuccess` throws 200 by design), breaking idempotency and enabling duplicate side effects.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

appSlug: "paystack",
traceContext,
});
} catch (processingError) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: New rollback catch resets payment.success even after successful processing (because handlePaymentSuccess throws 200 by design), breaking idempotency and enabling duplicate side effects.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/app-store/paystack/api/webhook.ts, line 145:

<comment>New rollback catch resets `payment.success` even after successful processing (because `handlePaymentSuccess` throws 200 by design), breaking idempotency and enabling duplicate side effects.</comment>

<file context>
@@ -120,31 +120,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
+        appSlug: "paystack",
+        traceContext,
+      });
+    } catch (processingError) {
+      // Rollback so webhook retries can re-process this payment
       await prisma.payment.update({
</file context>
Fix with Cubic

@Adebesin-Cell
Copy link
Copy Markdown

This would be a great addition 🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants