Overview
This webhook endpoint receives payment notification events from Paystack and automatically processes successful payments by creating subscriptions and sending invite links to users via Telegram.
Endpoint
POST /api/paystack/webhook
GET /api/paystack/webhook
Authentication
Signature Verification Required : All POST requests must include a valid x-paystack-signature header. Requests with missing or invalid signatures are rejected with 401 Unauthorized.
HMAC SHA512 signature of the raw request body, computed using your Paystack secret key
Signature Verification
The webhook verifies authenticity using HMAC SHA512:
const hash = crypto
. createHmac ( 'sha512' , PAYSTACK_SECRET_KEY )
. update ( rawBody )
. digest ( 'hex' )
if ( hash !== signature ) {
return 401 Unauthorized
}
The raw request body (before parsing) must be used for signature verification.
Request Body
Paystack sends webhook events with the following structure:
Event type. Supported: charge.success Other events are acknowledged but not processed.
Event payload containing payment details Payment status. Must be success for processing.
Unique transaction reference (e.g., TXN_1234567890) Used for idempotency - duplicate references are ignored.
Amount paid in kobo (smallest currency unit) Example: 500000 = ₦5,000
Payment channel used (e.g., card, bank, ussd)
ISO 8601 timestamp of payment
Customer information Customer’s first name (optional)
Customer’s last name (optional)
Customer-level metadata (alternative location for custom fields)
Transaction metadata containing custom fields data.metadata.telegram_id
Telegram user ID for automatic processing If missing, payment requires manual verification
data.metadata.telegram_username
Telegram username (optional)
Plan type: basic, biweekly, monthly, premium, or promo
data.metadata.customer_email
Customer’s email (may override data.customer.email)
data.metadata.custom_fields
Array of custom field objects Show Custom field structure
Field name (e.g., telegram_id, plan_type, customer_email)
Event Types
charge.success
Processed when payment is successful. Triggers automatic subscription creation.
Processing Logic:
Idempotency Check : Verify reference hasn’t been processed
Metadata Extraction : Extract telegram_id, plan_type, and customer_email
Manual Verification Fallback : If no telegram_id, log and skip automatic processing
Unban Previous Users : If user was previously removed, unban them
Invite Link Generation : Create one-time invite link via Telegram API
Subscription Creation : Save to database
Notification : Send invite link to user via Telegram
Other Events
All other event types (e.g., charge.failed, transfer.success) are acknowledged but not processed:
The webhook extracts metadata from multiple locations with this priority:
data.metadata.telegram_id (highest priority)
data.customer.metadata.telegram_id
data.metadata.custom_fields[] where variable_name === 'telegram_id'
custom_fields only exist in data.metadata, not data.customer.metadata
Plan Types
7 days access - ₦5,000 (500,000 kobo)
14 days access - ₦10,000 (1,000,000 kobo)
30 days access - ₦15,000 (1,500,000 kobo)
14 days access + MT5 Auto Copier - ₦22,000 (2,200,000 kobo) Sets hasCopierAccess: true in subscription
7 days access - ₦3,000 (300,000 kobo) Promotional pricing
Response Codes
Webhook processed successfully {
"status" : "already processed"
}
Automatic Processing Success
{
"success" : true ,
"message" : "Payment processed and invite link sent" ,
"telegramId" : "123456789" ,
"planType" : "premium"
}
Manual Verification Required
{
"status" : "received" ,
"message" : "Payment received but requires manual verification (no telegram_id in metadata)"
}
Missing or invalid signature {
"error" : "No signature provided"
}
{
"error" : "Invalid signature"
}
Processing failure {
"error" : "Failed to create invite link"
}
{
"error" : "Failed to save subscription"
}
{
"error" : "Webhook processing failed"
}
GET Handler
A simple health check endpoint:
curl https://your-domain.com/api/paystack/webhook
Response:
{
"status" : "Paystack webhook is running"
}
Database Schema
Successful webhook processing creates a Subscription record:
{
telegramUserId : string // From metadata.telegram_id
telegramUsername : string // From metadata.telegram_username
telegramName : string // From customer.first_name + customer.last_name
paystackRef : string // Unique - from data.reference
customerEmail : string // From data.customer.email
amountKobo : number // From data.amount
planType : PlanType // From metadata.plan_type
hasCopierAccess : boolean // true for 'premium' plan
startedAt : Date // Current timestamp
expiresAt : Date // Calculated based on plan duration
inviteLinkUsed : string // Generated invite link
}
Telegram Notification
After successful processing, the webhook sends this message to the user:
Standard Plans
Premium Plan
✅ Payment Verified Successfully!
💎 Plan: [PLAN_NAME]
💰 Amount: NGN [AMOUNT]
📅 Access expires: [EXPIRY_DATE]
Here is your one-time invite link (valid for 24 hours):
👉 [INVITE_LINK]
Click the link to join the channel. The link can only be used once.
Type /status anytime to check your subscription.
Idempotency
The webhook implements idempotency through reference checking:
const existingSubscription = await prisma . subscription . findFirst ({
where: { paystackRef: data . reference }
})
if ( existingSubscription ) {
return { status: 'already processed' }
}
Duplicate webhook deliveries with the same reference are safely ignored.
User Unbanning
If a user with previous removed subscriptions repays:
const previousRemovedSubscriptions = await prisma . subscription . findMany ({
where: {
telegramUserId: telegramId ,
isRemoved: true
}
})
if ( previousRemovedSubscriptions . length > 0 ) {
await unbanChatMember ( telegramId )
}
Expiry Calculation
Expiry dates are calculated based on plan type:
const expiresAt = calculateExpiryDate ( planType )
Plan Durations:
basic: 7 days
biweekly: 14 days
monthly: 30 days
premium: 14 days
promo: 7 days
Example Webhook Payloads
Successful Payment (Automatic Processing)
Payment Without Metadata (Manual Verification)
Payment with Custom Fields
{
"event" : "charge.success" ,
"data" : {
"status" : "success" ,
"reference" : "TXN_1234567890" ,
"amount" : 2200000 ,
"channel" : "bank" ,
"paid_at" : "2024-03-15T10:30:00.000Z" ,
"customer" : {
"email" : "[email protected] " ,
"first_name" : "John" ,
"last_name" : "Doe"
},
"metadata" : {
"telegram_id" : "987654321" ,
"telegram_username" : "johndoe" ,
"plan_type" : "premium" ,
"plan_name" : "Premium Plan" ,
"customer_email" : "[email protected] "
}
}
}
Error Scenarios
Missing Telegram ID
Webhook Payload:
{
"event" : "charge.success" ,
"data" : {
"reference" : "TXN_NO_METADATA" ,
"metadata" : {}
}
}
Response:
{
"status" : "received" ,
"message" : "Payment received but requires manual verification (no telegram_id in metadata)"
}
Consequence: User must manually verify payment using /verify_* commands in Telegram bot.
Duplicate Reference
Condition: Reference already exists in database
Response:
{
"status" : "already processed"
}
Consequence: No action taken, idempotent behavior.
Invite Link Creation Failure
Condition: Telegram API fails to create invite link
Response:
{
"error" : "Failed to create invite link"
}
HTTP Status: 500
Database Save Failure
Condition: Prisma throws error during subscription creation
Response:
{
"error" : "Failed to save subscription"
}
HTTP Status: 500
Security Best Practices
Always Verify Signatures : Never process webhooks without signature verification
Use HTTPS Only : Paystack only delivers webhooks to HTTPS endpoints
Keep Secret Key Secure : Store in environment variables, never commit to source control
Implement Idempotency : Always check for duplicate references
Log Suspicious Activity : Monitor for invalid signatures or unusual patterns
Testing
Paystack provides webhook testing tools in the dashboard:
Navigate to Settings > Webhooks
Use “Send Test” to trigger test events
Test signature verification with known payloads
Test webhooks use a different signature than production. Update your secret key accordingly.
Monitoring
Key metrics to monitor:
Signature verification failures (potential security issue)
Processing time (should be < 5 seconds)
Failed invite link creation rate
Database save failure rate
Manual verification rate (missing telegram_id)
Troubleshooting
Webhook not receiving events
Verify webhook URL is correct in Paystack dashboard
Ensure endpoint is publicly accessible via HTTPS
Check Paystack webhook logs for delivery failures
Verify firewall rules allow Paystack IPs
Verify PAYSTACK_SECRET_KEY environment variable is set correctly
Check that you’re using the correct secret key (test vs. live)
Ensure raw request body is used for signature verification
Verify signature header name is x-paystack-signature
Users not receiving invite links
Check that telegram_id is included in payment metadata
Verify Telegram bot token is valid
Ensure user hasn’t blocked the bot
Check Telegram API rate limits