Documentation Index Fetch the complete documentation index at: https://mintlify.com/revokslab/shipfree/llms.txt
Use this file to discover all available pages before exploring further.
The Payment Webhooks endpoint processes events from payment providers (Stripe, Polar, Lemon Squeezy) to keep your database synchronized with payment status changes.
Webhook Endpoint
POST /api/webhooks/payments
Receives and processes webhook events from payment providers.
Implementation Overview
From src/app/api/webhooks/payments/route.ts:10-225, the webhook handler:
Verifies webhook signature based on active provider
Parses the event into a standardized format
Processes the event via payment adapter
Updates database with customer, subscription, and payment data
Webhook Flow
export async function POST ( req : Request ) {
const rawBody = await req . text ()
const headerList = await headers ()
const adapter = getPaymentAdapter ()
// 1. Get signature based on provider
let signature = ''
switch ( adapter . provider ) {
case 'stripe' :
signature = headerList . get ( 'stripe-signature' ) || ''
break
case 'polar' :
signature = headerList . get ( 'polar-webhook-signature' ) || ''
break
case 'lemonsqueezy' :
signature = headerList . get ( 'x-signature' ) || ''
break
}
// 2. Validate signature
const isValid = await adapter . validateWebhook ( rawBody , signature )
if ( ! isValid ) {
return NextResponse . json ({ error: 'Invalid signature' }, { status: 400 })
}
// 3. Parse and process event
const event = parseWebhookEvent ( rawBody , adapter . provider )
const result = await adapter . processWebhook ( event )
// 4. Update database
await updateDatabase ( result )
return NextResponse . json ({ received: true })
}
Signature Verification
All webhook requests are verified using provider-specific signatures to ensure authenticity.
// Header: stripe-signature
const signature = headerList . get ( 'stripe-signature' )
// Verified using STRIPE_WEBHOOK_SECRET
await adapter . validateWebhook ( rawBody , signature )
From src/app/api/webhooks/payments/route.ts:22-42.
Supported Events
Customer Events
New customer created Updates: Creates new record in customer table
Customer information updated Updates: Updates email and metadata in customer table
Customer deleted Updates: Can cascade delete related records
Subscription Events
New subscription created Updates: Creates record in subscription table with status active or trialing
Subscription modified (plan change, status change, etc.) Updates: Updates subscription status, plan, billing period, amounts
Subscription canceled Updates: Sets status to canceled, records canceledAt timestamp
Subscription permanently deleted Updates: Marks subscription as deleted
Payment Events
invoice.payment_succeeded
Payment successful Updates: Creates payment record with status succeeded
Payment failed Updates: Creates or updates payment record with status failed, may update subscription to past_due
checkout.session.completed
Checkout session completed Updates: Creates customer, subscription, and initial payment records
One-time payment completed (Lemon Squeezy) Updates: Creates payment record for one-time purchase
From src/lib/payments/types.ts:109-120.
Database Updates
Customer Table
From src/app/api/webhooks/payments/route.ts:96-119:
if ( result . customer ) {
const existingCustomer = await db . query . customer . findFirst ({
where: eq ( customer . providerCustomerId , result . customer . providerCustomerId )
})
if ( existingCustomer ) {
// Update existing customer
await db . update ( customer )
. set ({
email: result . customer . email ,
updatedAt: new Date ()
})
. where ( eq ( customer . id , existingCustomer . id ))
} else if ( result . customer . userId ) {
// Create new customer
await db . insert ( customer ). values ({
id: result . customer . id ,
userId: result . customer . userId ,
provider: result . customer . provider ,
providerCustomerId: result . customer . providerCustomerId ,
email: result . customer . email
})
}
}
Subscription Table
From src/app/api/webhooks/payments/route.ts:122-181:
if ( result . subscription ) {
const existingSub = await db . query . subscription . findFirst ({
where: eq ( subscription . providerSubscriptionId , result . subscription . providerSubscriptionId )
})
if ( existingSub ) {
// Update existing subscription
await db . update ( subscription ). set ({
status: result . subscription . status ,
plan: result . subscription . plan ,
interval: result . subscription . interval ,
amount: result . subscription . amount ?. toString (),
currency: result . subscription . currency ,
currentPeriodStart: result . subscription . currentPeriodStart ,
currentPeriodEnd: result . subscription . currentPeriodEnd ,
cancelAtPeriodEnd: result . subscription . cancelAtPeriodEnd ,
canceledAt: result . subscription . canceledAt ,
trialStart: result . subscription . trialStart ,
trialEnd: result . subscription . trialEnd ,
updatedAt: new Date ()
}). where ( eq ( subscription . id , existingSub . id ))
} else if ( result . subscription . userId ) {
// Create new subscription
await db . insert ( subscription ). values ({ ... })
}
}
Payment Table
From src/app/api/webhooks/payments/route.ts:185-217:
if ( result . payment ) {
const existingPayment = await db . query . payment . findFirst ({
where: eq ( payment . providerPaymentId , result . payment . providerPaymentId )
})
if ( existingPayment ) {
// Update payment status
await db . update ( payment )
. set ({ status: result . payment . status , updatedAt: new Date () })
. where ( eq ( payment . id , existingPayment . id ))
} else if ( result . payment . userId ) {
// Create new payment
await db . insert ( payment ). values ({
id: result . payment . id ,
userId: result . payment . userId ,
customerId: result . payment . customerId ,
subscriptionId: result . payment . subscriptionId ,
provider: result . payment . provider ,
providerPaymentId: result . payment . providerPaymentId ,
type: result . payment . type ,
status: result . payment . status ,
amount: result . payment . amount . toString (),
currency: result . payment . currency ,
description: result . payment . description
})
}
}
Webhook Event Structure
From src/lib/payments/types.ts:125-130:
export interface WebhookEvent {
type : WebhookEventType
provider : PaymentProvider
data : any // Provider-specific event data
rawEvent : any // Raw provider event
}
Provider-Specific Parsing
From src/app/api/webhooks/payments/route.ts:56-80:
const parsedBody = JSON . parse ( rawBody )
event = {
type: parsedBody . type ,
provider: 'stripe' ,
data: parsedBody . data . object ,
rawEvent: parsedBody
}
Setup Instructions
Configure Webhook URL
Add webhook endpoint to your payment provider: https://yourapp.com/api/webhooks/payments
Set Webhook Secret
Add the webhook signing secret to your environment variables: STRIPE_WEBHOOK_SECRET = whsec_...
Select Events
Configure which events to receive (recommended):
customer.created
customer.updated
subscription.created
subscription.updated
subscription.deleted
invoice.payment_succeeded
invoice.payment_failed
checkout.session.completed
Test Webhook
Use provider’s test mode to verify webhook is working: stripe trigger checkout.session.completed
Success Response
Status: 200 OK
Error Responses
Missing or invalid signature { "error" : "Missing signature" }
{ "error" : "Invalid signature" }
{ "error" : "Invalid JSON" }
Webhook processing error {
"error" : "Failed to process subscription.updated event"
}
Idempotency
The webhook handler uses providerCustomerId, providerSubscriptionId, and providerPaymentId to ensure idempotency. Duplicate events are handled gracefully by updating existing records.
From src/app/api/webhooks/payments/route.ts:97-99, 123-125, 186-188:
// Check if record exists before creating
const existing = await db . query . customer . findFirst ({
where: eq ( customer . providerCustomerId , providerCustomerId )
})
if ( existing ) {
// Update instead of creating duplicate
await db . update ( customer ). set ({ ... })
}
Debugging
Webhook errors are logged to console:
console . error ( 'Webhook processing error:' , result . error )
console . error ( 'Webhook handler error:' , error )
From src/app/api/webhooks/payments/route.ts:89, 222.
Enable detailed logging in development to troubleshoot webhook issues. Check your payment provider’s dashboard for webhook delivery status and retry attempts.
Database Schema View customer, subscription, and payment tables
Checkout API Create checkout sessions
Payment Types TypeScript type definitions