Skip to main content
POST
/
api
/
paystack
/
webhook
Paystack Webhook
curl --request POST \
  --url https://api.example.com/api/paystack/webhook \
  --header 'Content-Type: <content-type>' \
  --header 'x-paystack-signature: <x-paystack-signature>' \
  --data '
{
  "event": "<string>",
  "data": {
    "data.status": "<string>",
    "data.reference": "<string>",
    "data.amount": 123,
    "data.channel": "<string>",
    "data.paid_at": "<string>",
    "data.customer": {
      "data.customer.email": "<string>",
      "data.customer.first_name": "<string>",
      "data.customer.last_name": "<string>",
      "data.customer.metadata": {}
    },
    "data.metadata": {
      "data.metadata.telegram_id": "<string>",
      "data.metadata.telegram_username": "<string>",
      "data.metadata.plan_type": "<string>",
      "data.metadata.plan_name": "<string>",
      "data.metadata.customer_email": "<string>",
      "data.metadata.custom_fields": [
        {
          "variable_name": "<string>",
          "value": "<string>"
        }
      ]
    }
  }
}
'
{
  "200": {},
  "401": {},
  "500": {},
  "basic": "<string>",
  "biweekly": "<string>",
  "monthly": "<string>",
  "premium": "<string>",
  "promo": "<string>"
}

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.

Request Headers

x-paystack-signature
string
required
HMAC SHA512 signature of the raw request body, computed using your Paystack secret key
Content-Type
string
required
Must be application/json

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
string
required
Event type. Supported: charge.successOther events are acknowledged but not processed.
data
object
required
Event payload containing payment details

Event Types

charge.success

Processed when payment is successful. Triggers automatic subscription creation. Processing Logic:
  1. Idempotency Check: Verify reference hasn’t been processed
  2. Metadata Extraction: Extract telegram_id, plan_type, and customer_email
  3. Manual Verification Fallback: If no telegram_id, log and skip automatic processing
  4. Unban Previous Users: If user was previously removed, unban them
  5. Invite Link Generation: Create one-time invite link via Telegram API
  6. Subscription Creation: Save to database
  7. 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:
{
  "received": true
}

Metadata Priority

The webhook extracts metadata from multiple locations with this priority:
  1. data.metadata.telegram_id (highest priority)
  2. data.customer.metadata.telegram_id
  3. data.metadata.custom_fields[] where variable_name === 'telegram_id'
custom_fields only exist in data.metadata, not data.customer.metadata

Plan Types

basic
string
7 days access - ₦5,000 (500,000 kobo)
biweekly
string
14 days access - ₦10,000 (1,000,000 kobo)
monthly
string
30 days access - ₦15,000 (1,500,000 kobo)
premium
string
14 days access + MT5 Auto Copier - ₦22,000 (2,200,000 kobo)Sets hasCopierAccess: true in subscription
promo
string
7 days access - ₦3,000 (300,000 kobo)Promotional pricing

Response Codes

200
Success
Webhook processed successfully
401
Unauthorized
Missing or invalid signature
500
Internal Server Error
Processing failure

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:
✅ 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

{
  "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. 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

  1. Always Verify Signatures: Never process webhooks without signature verification
  2. Use HTTPS Only: Paystack only delivers webhooks to HTTPS endpoints
  3. Keep Secret Key Secure: Store in environment variables, never commit to source control
  4. Implement Idempotency: Always check for duplicate references
  5. Log Suspicious Activity: Monitor for invalid signatures or unusual patterns

Testing

Paystack provides webhook testing tools in the dashboard:
  1. Navigate to Settings > Webhooks
  2. Use “Send Test” to trigger test events
  3. 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

  1. Verify webhook URL is correct in Paystack dashboard
  2. Ensure endpoint is publicly accessible via HTTPS
  3. Check Paystack webhook logs for delivery failures
  4. Verify firewall rules allow Paystack IPs
  1. Verify PAYSTACK_SECRET_KEY environment variable is set correctly
  2. Check that you’re using the correct secret key (test vs. live)
  3. Ensure raw request body is used for signature verification
  4. Verify signature header name is x-paystack-signature

Build docs developers (and LLMs) love