Reddot UI Library
Recurly Payment Integration
Complete Recurly integration with client, webhooks, and checkout UI for Next.js applications
Recurly is a subscription management and recurring billing platform. This integration provides a complete payment solution for Next.js applications, including client-side tokenization, server-side billing, webhook handling, and React components.
What's Included
This integration is split into three modular blocks:
- recurly-core: Backend services (client, webhooks, API handlers)
- recurly-hooks: React hooks for Recurly.js integration
- recurly-checkout: Ready-to-use checkout UI components
Installation
Backend Only
If you only need backend functionality (creating accounts, handling webhooks):
Full Stack (Recommended)
For a complete payment flow with UI components:
npx shadcn@latest add https://reddot.dottools.xyz/r/recurly-core.json
npx shadcn@latest add https://reddot.dottools.xyz/r/recurly-hooks.json
npx shadcn@latest add https://reddot.dottools.xyz/r/recurly-checkout.jsonConfiguration
Add the following environment variables to your .env.local:
# Recurly Public Key (client-side)
NEXT_PUBLIC_RECURLY_PUBLIC_KEY=ewr1-xxxxxxxxxxxxx
# Recurly API Key (server-side)
NEXT_SSR_RECURLY_API_KEY=xxxxxxxxxxxxx
# Recurly Webhook Signing Key (server-side)
NEXT_SSR_RECURLY_WEBHOOK_KEY=xxxxxxxxxxxxxYou can find these credentials in your Recurly Dashboard:
- Public Key: Developers > Credentials > Public Key
- API Key: Developers > Credentials > Private API Key
- Webhook Key: Configuration > Webhooks > Signing Keys
Structure
recurly-core
lib/recurly/
├── client.ts # Recurly client (server-side)
├── types.ts # TypeScript types
├── schemas.ts # Zod validation schemas
├── actions/
│ └── create-account.ts # Server Action for account creation
├── api/
│ └── fetch-plans.ts # Fetch Recurly plans
└── webhook/
├── types.ts # Webhook payload types
├── verify-signature.ts # Signature verification
└── handlers/
├── index.ts # Main webhook router
├── account.ts # Account event handlers
├── subscription.ts # Subscription event handlers
├── invoice.ts # Invoice event handlers
└── payment.ts # Payment event handlers
recurly-hooks
hooks/recurly/
├── use-recurly-script.tsx # Loads Recurly.js script
├── use-recurly.tsx # Initializes Recurly instance
└── use-recurly-elements.tsx # Tokenization helper
recurly-checkout
components/recurly-checkout/
├── checkout.tsx # Complete checkout component
├── payment-form.tsx # Payment form with card elements
└── elements/
├── card-number.tsx # Card number input
├── card-cvc.tsx # CVC input
├── card-month.tsx # Expiry month input
└── card-year.tsx # Expiry year input
Usage
Creating a Subscription (Backend)
'use server';
import { createRecurlyAccount } from '@/lib/recurly/actions/create-account';
export async function handleCheckout(formData: FormData) {
const result = await createRecurlyAccount({
token: formData.get('recurly_token') as string,
email: formData.get('email') as string,
firstName: formData.get('first_name') as string,
lastName: formData.get('last_name') as string,
currency: 'CHF',
});
if (result.success) {
console.log('Account created:', result.accountId);
console.log('Subscription created:', result.subscriptionId);
} else {
console.error('Payment failed:', result.error);
}
return result;
}Fetching Plans
import { fetchRecurlyPlans } from '@/lib/recurly/api/fetch-plans';
export async function getPlans() {
try {
const plans = await fetchRecurlyPlans();
return plans;
} catch (error) {
console.error('Failed to fetch plans:', error);
return [];
}
}Checkout UI Component
'use client';
import { RecurlyCheckout } from '@/components/recurly-checkout/checkout';
import { useRouter } from 'next/navigation';
export function CheckoutPage() {
const router = useRouter();
return (
<RecurlyCheckout
publicKey={process.env.NEXT_PUBLIC_RECURLY_PUBLIC_KEY!}
onSuccess={(result) => {
console.log('Payment successful!', result);
router.push('/checkout/success');
}}
onError={(error) => {
console.error('Payment failed:', error);
}}
>
<button type="submit" className="w-full mt-4 px-4 py-2 bg-blue-600 text-white rounded">
Complete Purchase
</button>
</RecurlyCheckout>
);
}Custom Payment Form
'use client';
import { useRecurlyScript } from '@/hooks/recurly/use-recurly-script';
import { useRecurly } from '@/hooks/recurly/use-recurly';
import { RecurlyPaymentForm } from '@/components/recurly-checkout/payment-form';
export function CustomCheckout() {
const { isLoaded, isLoading, error: scriptError } = useRecurlyScript();
const { elements, isReady } = useRecurly(process.env.NEXT_PUBLIC_RECURLY_PUBLIC_KEY!);
if (isLoading) return <div>Loading...</div>;
if (scriptError) return <div>Error loading payment system</div>;
if (!isReady || !elements) return <div>Initializing...</div>;
const handleSubmit = async (formData: FormData) => {
// Handle form submission
console.log('Form data:', formData);
};
return (
<RecurlyPaymentForm
elements={elements}
onSubmit={handleSubmit}
isSubmitting={false}
>
<button type="submit">Pay Now</button>
</RecurlyPaymentForm>
);
}Webhooks
Setting up Webhook Endpoint
Create an API route at app/api/webhooks/recurly/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { verifyRecurlySignature } from '@/lib/recurly/webhook/verify-signature';
import { RecurlyWebhookPayloadSchema } from '@/lib/recurly/webhook/types';
import { handleWebhookEvent } from '@/lib/recurly/webhook/handlers';
export async function POST(request: NextRequest) {
try {
const webhookSecret = process.env.NEXT_SSR_RECURLY_WEBHOOK_KEY;
if (!webhookSecret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const rawBody = await request.text();
const recurlySignatureHeader = request.headers.get('recurly-signature');
// Verify signature
const isValid = verifyRecurlySignature(recurlySignatureHeader, rawBody, webhookSecret);
if (!isValid) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// Parse and validate payload
const payload = RecurlyWebhookPayloadSchema.parse(JSON.parse(rawBody));
// Handle the event
const result = await handleWebhookEvent(payload);
return NextResponse.json({ received: true, handled: result.handled }, { status: 200 });
} catch (error) {
console.error('[Recurly Webhook] Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}Configuring Webhooks in Recurly
- Go to your Recurly Dashboard > Configuration > Webhooks
- Add a new webhook endpoint:
https://your-domain.com/api/webhooks/recurly - Select the events you want to receive
- Copy the signing key and add it to
NEXT_SSR_RECURLY_WEBHOOK_KEY
Supported Events
The webhook handler supports the following event types:
Account Events:
new_accountbilling_info_updatedreactivated_accountcanceled_account
Subscription Events:
new_subscriptionupdated_subscriptionrenewed_subscriptioncanceled_subscriptionexpired_subscription
Invoice Events:
new_invoiceprocessing_invoiceclosed_invoicepast_due_invoice
Payment Events:
successful_paymentfailed_paymentvoid_paymentsuccessful_refund
Customizing Webhook Handlers
You can customize the webhook handlers in lib/recurly/webhook/handlers/:
// lib/recurly/webhook/handlers/subscription.ts
async function handleNewSubscription(payload: SubscriptionWebhookPayload) {
console.log('New subscription created:', {
subscription_id: payload.subscription?.id,
account_id: payload.subscription?.account_id,
plan_code: payload.subscription?.plan_code,
});
// Add your custom logic here
// For example: send welcome email, update database, etc.
return {
success: true,
handled: true,
};
}Security
Signature Verification
All webhook payloads are verified using HMAC-SHA256 signatures. The signature verification:
- Validates the timestamp (5-minute tolerance)
- Prevents replay attacks
- Uses timing-safe comparison
Environment Variables
- Never expose
NEXT_SSR_RECURLY_API_KEYorNEXT_SSR_RECURLY_WEBHOOK_KEYto the client - Only
NEXT_PUBLIC_RECURLY_PUBLIC_KEYshould be client-accessible - Store sensitive keys in environment variables, not in code
Tokenization
Payment card data never touches your server:
- Recurly.js tokenizes card details in the browser
- Only the token is sent to your server
- Your server uses the token to create the subscription
Error Handling
The integration includes user-friendly error messages for common payment failures:
{
success: false,
error: 'Your card has been declined. Please check your information or use another card.',
code: 'CARD_DECLINED'
}Supported error codes:
CARD_DECLINEDINSUFFICIENT_FUNDSCARD_EXPIREDINVALID_CARDINVALID_CVVPAYMENT_ERROR(generic)
TypeScript Support
All types are fully typed with TypeScript:
import type {
CreateRecurlyAccountInput,
CreateRecurlyAccountResponse,
RecurlyPlan,
RecurlyWebhookPayload,
} from '@/lib/recurly/types';Resources
Troubleshooting
"Recurly.js not loaded" Error
Make sure the Recurly script is loaded before using the hooks:
const { isLoaded, isLoading } = useRecurlyScript();
if (!isLoaded) {
return <div>Loading payment system...</div>;
}Webhook Signature Verification Fails
- Verify
NEXT_SSR_RECURLY_WEBHOOK_KEYmatches your Recurly dashboard - Ensure you're using the raw request body (not parsed JSON)
- Check your server time is synchronized (NTP)
Build Errors
If you see import errors during build, make sure registry is excluded in tsconfig.json:
{
"exclude": ["node_modules", "registry"]
}