handle-webhooks
π―Skillfrom payram/payram-helper-mcp-server
Enables real-time payment status updates by securely implementing webhook handlers across multiple frameworks for Payram integrations.
Part of
payram/payram-helper-mcp-server(14 items)
Installation
python scripts/test_webhook.pySkill Details
Complete guide to implementing Payram webhook handlers across all supported frameworks for real-time payment status updates
Overview
# Handle Payram Webhooks
Overview
This skill provides comprehensive instructions for implementing Payram webhook handlers in your application. Webhooks enable real-time notifications when payment status changes, eliminating the need for polling. You'll learn how to create secure webhook endpoints, validate incoming requests, route events to appropriate handlers, and test your implementation across all supported frameworks.
When to Use This Skill
Use this skill when you need to:
- Receive real-time payment status notifications
- Eliminate polling for payment updates
- Implement event-driven payment processing
- Handle payment lifecycle events (filled, cancelled, etc.)
- Build production-ready webhook endpoints with proper security
- Test webhook integration locally before deploying
Prerequisites
Before starting, ensure you have:
- Completed the
setup-payramskill (environment configured) - Created payment functionality (
integrate-paymentsskill) - Configured
PAYRAM_WEBHOOK_SECRETin your.envfile - Exposed a publicly accessible HTTPS endpoint (for production)
- Reviewed
docs/payram-webhook.yamlfor webhook payload schema
---
Instructions
Part 1: Understanding Webhook Flow
#### 1.1 Webhook Delivery
When a payment status changes, Payram sends an HTTP POST request:
```
POST https://your-domain.com/api/payram/webhook
Content-Type: application/json
API-Key: your-webhook-secret
{
"reference_id": "ref_abc123",
"invoice_id": "inv_xyz456",
"customer_id": "cust_123",
"customer_email": "customer@example.com",
"status": "FILLED",
"amount": 49.99,
"filled_amount_in_usd": 49.99,
"currency": "USD"
}
```
Action: Configure your webhook URL in Payram dashboard and store the shared secret in PAYRAM_WEBHOOK_SECRET.
#### 1.2 Payment Status Events
Webhooks notify you of these payment states:
- OPEN - Payment created, awaiting customer action
- FILLED - Payment completed successfully (exact amount paid)
- PARTIALLY_FILLED - Partial payment received (less than requested)
- OVER_FILLED - Overpayment received (more than requested)
- CANCELLED - Payment cancelled by customer or merchant
- UNDEFINED - Unknown status (future compatibility)
Action: Implement handlers for each status based on your business logic.
#### 1.3 Security Verification
All webhook requests include an API-Key header:
```
API-Key: your-webhook-secret-from-payram-dashboard
```
Critical: Always verify this header matches your stored secret before processing events.
---
Part 2: TypeScript Type Definitions
#### 2.1 Create Webhook Types
File: src/types/payramWebhook.ts (or lib/payram/webhookTypes.ts for Next.js)
```typescript
export type PayramWebhookStatus =
| 'OPEN'
| 'CANCELLED'
| 'FILLED'
| 'PARTIALLY_FILLED'
| 'OVER_FILLED'
| 'UNDEFINED';
export interface PayramWebhookPayload {
reference_id: string;
invoice_id?: string;
customer_id?: string;
customer_email?: string;
status: PayramWebhookStatus;
amount?: number;
filled_amount_in_usd?: number;
currency?: string; // 3-letter ISO code
[key: string]: unknown; // Allow additional fields
}
export interface PayramWebhookAck {
message: string;
}
```
---
Part 3: Event Router Implementation
#### 3.1 Create Status-Based Router
File: src/services/payramWebhookRouter.ts
```typescript
import { PayramWebhookPayload } from '../types/payramWebhook';
export async function handlePayramEvent(payload: PayramWebhookPayload) {
console.log('Received Payram webhook:', payload.reference_id, payload.status);
switch (payload.status) {
case 'FILLED':
await handleFilledPayment(payload);
break;
case 'OPEN':
await handleOpenPayment(payload);
break;
case 'PARTIALLY_FILLED':
await handlePartialPayment(payload);
break;
case 'OVER_FILLED':
await handleOverfilledPayment(payload);
break;
case 'CANCELLED':
await handleCancelledPayment(payload);
break;
case 'UNDEFINED':
default:
await handleUndefinedStatus(payload);
break;
}
}
async function handleFilledPayment(payload: PayramWebhookPayload) {
// TODO: Mark order as paid, deliver goods, send confirmation email
console.log('β Payment FILLED:', payload.reference_id);
// Example: Update database
// await db.payments.update({
// where: { payramReferenceId: payload.reference_id },
// data: { status: 'completed', paidAt: new Date() }
// });
// Example: Fulfill order
// await fulfillOrder(payload.customer_id, payload.reference_id);
}
async function handleOpenPayment(payload: PayramWebhookPayload) {
// TODO: Record that payment request was acknowledged
console.log('π΅ Payment OPEN:', payload.reference_id);
// Example: Update database
// await db.payments.update({
// where: { payramReferenceId: payload.reference_id },
// data: { status: 'open', openedAt: new Date() }
// });
}
async function handlePartialPayment(payload: PayramWebhookPayload) {
// TODO: Update outstanding balance, notify finance team
console.log('β οΈ Payment PARTIALLY_FILLED:', payload.reference_id, payload.filled_amount_in_usd);
// Example: Calculate remaining amount
// const remaining = (payload.amount || 0) - (payload.filled_amount_in_usd || 0);
// await notifyFinance(Partial payment received. Remaining: $${remaining});
}
async function handleOverfilledPayment(payload: PayramWebhookPayload) {
// TODO: Queue manual review or process refund
console.log('π° Payment OVER_FILLED:', payload.reference_id, payload.filled_amount_in_usd);
// Example: Queue refund
// const excess = (payload.filled_amount_in_usd || 0) - (payload.amount || 0);
// await queueRefund(payload.customer_id, excess);
}
async function handleCancelledPayment(payload: PayramWebhookPayload) {
// TODO: Release inventory, notify customer
console.log('β Payment CANCELLED:', payload.reference_id);
// Example: Update database and release inventory
// await db.payments.update({
// where: { payramReferenceId: payload.reference_id },
// data: { status: 'cancelled', cancelledAt: new Date() }
// });
// await releaseInventory(payload.customer_id, payload.reference_id);
}
async function handleUndefinedStatus(payload: PayramWebhookPayload) {
// TODO: Log for investigation
console.warn('β‘ Payment status UNDEFINED:', payload.reference_id, payload);
// Example: Alert monitoring system
// await alertMonitoring('Unknown payment status received', payload);
}
```
---
Part 4: Framework-Specific Handlers
#### 4.1 Express.js Handler
File: src/routes/payramWebhook.ts
```typescript
import express, { Request, Response } from 'express';
import { handlePayramEvent } from '../services/payramWebhookRouter';
import { PayramWebhookPayload, PayramWebhookAck } from '../types/payramWebhook';
const router = express.Router();
router.use(express.json());
router.post('/api/payram/webhook', async (req: Request, res: Response) => {
// 1. Verify webhook secret is configured
const sharedSecret = process.env.PAYRAM_WEBHOOK_SECRET;
if (!sharedSecret) {
console.error('PAYRAM_WEBHOOK_SECRET not configured');
return res.status(500).json({ error: 'webhook_not_configured' });
}
// 2. Validate API-Key header
const incomingKey = req.get('API-Key');
if (!incomingKey || incomingKey !== sharedSecret) {
console.warn('Invalid webhook API-Key received');
return res.status(401).json({ error: 'invalid-webhook-key' });
}
// 3. Validate payload structure
const payload = req.body as PayramWebhookPayload;
if (!payload?.reference_id || !payload?.status) {
console.error('Invalid webhook payload:', payload);
return res.status(400).json({ error: 'invalid-webhook-payload' });
}
// 4. Process event
try {
await handlePayramEvent(payload);
const ack: PayramWebhookAck = { message: 'Webhook received successfully' };
return res.json(ack);
} catch (error) {
console.error('Error handling Payram webhook', error);
return res.status(500).json({ error: 'webhook_handler_error' });
}
});
export default router;
```
Integration: Wire into Express app:
```typescript
import payramWebhookRouter from './routes/payramWebhook';
app.use(payramWebhookRouter);
```
#### 4.2 Next.js App Router Handler
File: app/api/payram/webhook/route.ts
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { handlePayramEvent } from '@/lib/payram/handlePayramEvent';
import { PayramWebhookPayload, PayramWebhookAck } from '@/lib/payram/webhookTypes';
export async function POST(request: NextRequest) {
// 1. Verify webhook secret is configured
const sharedSecret = process.env.PAYRAM_WEBHOOK_SECRET;
if (!sharedSecret) {
console.error('PAYRAM_WEBHOOK_SECRET not configured');
return NextResponse.json({ error: 'webhook_not_configured' }, { status: 500 });
}
// 2. Validate API-Key header
const incomingKey = request.headers.get('API-Key');
if (!incomingKey || incomingKey !== sharedSecret) {
console.warn('Invalid webhook API-Key received');
return NextResponse.json({ error: 'invalid-webhook-key' }, { status: 401 });
}
// 3. Parse and validate payload
let payload: PayramWebhookPayload;
try {
payload = (await request.json()) as PayramWebhookPayload;
} catch (error) {
console.error('Failed to parse webhook JSON:', error);
return NextResponse.json({ error: 'invalid-json-payload' }, { status: 400 });
}
if (!payload.reference_id || !payload.status) {
console.error('Invalid webhook payload:', payload);
return NextResponse.json({ error: 'invalid-webhook-payload' }, { status: 400 });
}
// 4. Process event
try {
await handlePayramEvent(payload);
const ack: PayramWebhookAck = { message: 'Webhook received successfully' };
return NextResponse.json(ack);
} catch (error) {
console.error('Error handling Payram webhook', error);
return NextResponse.json({ error: 'webhook_handler_error' }, { status: 500 });
}
}
```
#### 4.3 FastAPI Handler
File: app/webhooks/payram.py
```python
import os
from fastapi import FastAPI, HTTPException, Request
from app.services.payram_webhook_router import handle_payram_event
app = FastAPI()
@app.post('/api/payram/webhook')
async def payram_webhook(request: Request):
# 1. Verify webhook secret is configured
shared_secret = os.getenv('PAYRAM_WEBHOOK_SECRET')
if not shared_secret:
raise HTTPException(status_code=500, detail='webhook_not_configured')
# 2. Validate API-Key header
incoming_key = request.headers.get('API-Key')
if not incoming_key or incoming_key != shared_secret:
raise HTTPException(status_code=401, detail='invalid-webhook-key')
# 3. Parse and validate payload
payload = await request.json()
if 'reference_id' not in payload or 'status' not in payload:
raise HTTPException(status_code=400, detail='invalid-webhook-payload')
# 4. Process event
await handle_payram_event(payload)
return {'message': 'Webhook received successfully'}
```
Event Router: app/services/payram_webhook_router.py
```python
async def handle_payram_event(payload: dict):
status = payload.get('status')
reference_id = payload.get('reference_id')
if status == 'FILLED':
await handle_filled_payment(payload)
elif status == 'OPEN':
await handle_open_payment(payload)
elif status == 'PARTIALLY_FILLED':
await handle_partial_payment(payload)
elif status == 'OVER_FILLED':
await handle_overfilled_payment(payload)
elif status == 'CANCELLED':
await handle_cancelled_payment(payload)
else:
await handle_undefined_status(payload)
async def handle_filled_payment(payload: dict):
print(f"β Payment FILLED: {payload['reference_id']}")
# TODO: Mark order as paid, fulfill order
async def handle_open_payment(payload: dict):
print(f"π΅ Payment OPEN: {payload['reference_id']}")
# TODO: Record payment acknowledgement
async def handle_partial_payment(payload: dict):
print(f"β οΈ Payment PARTIALLY_FILLED: {payload['reference_id']}")
# TODO: Update outstanding balance
async def handle_overfilled_payment(payload: dict):
print(f"π° Payment OVER_FILLED: {payload['reference_id']}")
# TODO: Queue refund
async def handle_cancelled_payment(payload: dict):
print(f"β Payment CANCELLED: {payload['reference_id']}")
# TODO: Release inventory
async def handle_undefined_status(payload: dict):
print(f"β‘ Unknown status: {payload.get('status')}")
# TODO: Log for investigation
```
#### 4.4 Gin (Go) Handler
File: internal/webhooks/payram.go
```go
package webhooks
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type PayramWebhookPayload struct {
ReferenceID string json:"reference_id" binding:"required"
InvoiceID *string json:"invoice_id"
CustomerID *string json:"customer_id"
CustomerEmail *string json:"customer_email"
Status string json:"status" binding:"required"
Amount *float64 json:"amount"
FilledAmountInUSD *float64 json:"filled_amount_in_usd"
Currency *string json:"currency"
}
func RegisterPayramRoutes(router *gin.Engine) {
router.POST("/api/payram/webhook", handlePayramWebhook)
}
func handlePayramWebhook(c *gin.Context) {
// 1. Verify webhook secret is configured
sharedSecret := os.Getenv("PAYRAM_WEBHOOK_SECRET")
if sharedSecret == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "webhook_not_configured"})
return
}
// 2. Validate API-Key header
if c.GetHeader("API-Key") != sharedSecret {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid-webhook-key"})
return
}
// 3. Parse and validate payload
var payload PayramWebhookPayload
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid-json-payload"})
return
}
// 4. Process event
if err := handlePayramEvent(payload); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "webhook_handler_error"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Webhook received successfully"})
}
func handlePayramEvent(payload PayramWebhookPayload) error {
switch payload.Status {
case "FILLED":
return handleFilledPayment(payload)
case "OPEN":
return handleOpenPayment(payload)
case "PARTIALLY_FILLED":
return handlePartialPayment(payload)
case "OVER_FILLED":
return handleOverfilledPayment(payload)
case "CANCELLED":
return handleCancelledPayment(payload)
default:
return handleUndefinedStatus(payload)
}
}
func handleFilledPayment(payload PayramWebhookPayload) error {
// TODO: Mark order as paid
return nil
}
// Implement other handlers...
```
#### 4.5 Laravel Handler
File: routes/api.php
```php
use App\Http\Controllers\PayramWebhookController;
Route::post('/api/payram/webhook', [PayramWebhookController::class, 'handle']);
```
File: app/Http/Controllers/PayramWebhookController.php
```php
namespace App\Http\Controllers;
use App\Services\PayramWebhookRouter;
use Illuminate\Http\Request;
class PayramWebhookController extends Controller
{
public function __construct(private PayramWebhookRouter $router)
{
}
public function handle(Request $request)
{
// 1. Verify webhook secret is configured
$sharedSecret = env('PAYRAM_WEBHOOK_SECRET');
if (!$sharedSecret) {
return response()->json(['error' => 'webhook_not_configured'], 500);
}
// 2. Validate API-Key header
if ($request->header('API-Key') !== $sharedSecret) {
return response()->json(['error' => 'invalid-webhook-key'], 401);
}
// 3. Validate payload structure
$payload = $request->json()->all();
if (empty($payload['reference_id']) || empty($payload['status'])) {
return response()->json(['error' => 'invalid-webhook-payload'], 400);
}
// 4. Process event
$this->router->handle($payload);
return response()->json(['message' => 'Webhook received successfully']);
}
}
```
File: app/Services/PayramWebhookRouter.php
```php
namespace App\Services;
class PayramWebhookRouter
{
public function handle(array $payload): void
{
$status = $payload['status'] ?? null;
match ($status) {
'FILLED' => $this->handleFilledPayment($payload),
'OPEN' => $this->handleOpenPayment($payload),
'PARTIALLY_FILLED' => $this->handlePartialPayment($payload),
'OVER_FILLED' => $this->handleOverfilledPayment($payload),
'CANCELLED' => $this->handleCancelledPayment($payload),
default => $this->handleUndefinedStatus($payload),
};
}
private function handleFilledPayment(array $payload): void
{
// TODO: Mark order as paid
\Log::info('Payment FILLED: ' . $payload['reference_id']);
}
// Implement other handlers...
}
```
#### 4.6 Spring Boot Handler
File: src/main/java/com/example/webhooks/PayramWebhookController.java
```java
package com.example.webhooks;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/payram")
public class PayramWebhookController {
private final PayramWebhookRouter router;
public PayramWebhookController(PayramWebhookRouter router) {
this.router = router;
}
@PostMapping("/webhook")
public ResponseEntity> handleWebhook(
@RequestBody Map
@RequestHeader(value = "API-Key", required = false) String apiKey) {
// 1. Verify webhook secret is configured
String sharedSecret = System.getenv("PAYRAM_WEBHOOK_SECRET");
if (sharedSecret == null || sharedSecret.isBlank()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "webhook_not_configured"));
}
// 2. Validate API-Key header
if (apiKey == null || !apiKey.equals(sharedSecret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "invalid-webhook-key"));
}
// 3. Validate payload structure
if (!payload.containsKey("reference_id") || !payload.containsKey("status")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "invalid-webhook-payload"));
}
// 4. Process event
router.handle(payload);
return ResponseEntity.ok(Map.of("message", "Webhook received successfully"));
}
}
```
File: src/main/java/com/example/webhooks/PayramWebhookRouter.java
```java
package com.example.webhooks;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class PayramWebhookRouter {
public void handle(Map
String status = (String) payload.get("status");
switch (status) {
case "FILLED" -> handleFilledPayment(payload);
case "OPEN" -> handleOpenPayment(payload);
case "PARTIALLY_FILLED" -> handlePartialPayment(payload);
case "OVER_FILLED" -> handleOverfilledPayment(payload);
case "CANCELLED" -> handleCancelledPayment(payload);
default -> handleUndefinedStatus(payload);
}
}
private void handleFilledPayment(Map
// TODO: Mark order as paid
System.out.println("Payment FILLED: " + payload.get("reference_id"));
}
// Implement other handlers...
}
```
---
Part 5: Local Testing
#### 5.1 Using curl
File: scripts/test-webhook.sh
```bash
#!/bin/bash
# Set your local webhook URL and secret
WEBHOOK_URL="${MOCK_WEBHOOK_URL:-http://localhost:3000/api/payram/webhook}"
WEBHOOK_SECRET="${PAYRAM_WEBHOOK_SECRET:-your-webhook-secret}"
# Send mock FILLED event
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "API-Key: $WEBHOOK_SECRET" \
-d '{
"reference_id": "ref_test_001",
"invoice_id": "inv_test_001",
"customer_id": "cust_123",
"customer_email": "test@example.com",
"status": "FILLED",
"amount": 49.99,
"filled_amount_in_usd": 49.99,
"currency": "USD"
}'
```
Usage:
```bash
chmod +x scripts/test-webhook.sh
./scripts/test-webhook.sh
```
#### 5.2 Using Python
File: scripts/test_webhook.py
```python
import os
import requests
WEBHOOK_URL = os.getenv('MOCK_WEBHOOK_URL', 'http://localhost:3000/api/payram/webhook')
WEBHOOK_SECRET = os.getenv('PAYRAM_WEBHOOK_SECRET', 'your-webhook-secret')
def test_filled_event():
payload = {
'reference_id': 'ref_test_001',
'invoice_id': 'inv_test_001',
'customer_id': 'cust_123',
'customer_email': 'test@example.com',
'status': 'FILLED',
'amount': 49.99,
'filled_amount_in_usd': 49.99,
'currency': 'USD',
}
response = requests.post(
WEBHOOK_URL,
headers={
'Content-Type': 'application/json',
'API-Key': WEBHOOK_SECRET
},
json=payload,
)
print(f'Status: {response.status_code}')
print(f'Response: {response.text}')
if __name__ == '__main__':
test_filled_event()
```
Usage:
```bash
python scripts/test_webhook.py
```
#### 5.3 Test All Status Events
Create a test suite that sends all possible status events:
```typescript
// scripts/test-all-webhooks.ts
const statuses = ['OPEN', 'FILLED', 'PARTIALLY_FILLED', 'OVER_FILLED', 'CANCELLED'];
async function testAllWebhooks() {
for (const status of statuses) {
const payload = {
reference_id: ref_test_${status.toLowerCase()},
status,
customer_id: 'cust_test',
amount: 100,
filled_amount_in_usd: status === 'PARTIALLY_FILLED' ? 50 : 100,
};
const response = await fetch('http://localhost:3000/api/payram/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.PAYRAM_WEBHOOK_SECRET!,
},
body: JSON.stringify(payload),
});
console.log(${status}: ${response.status} - ${await response.text()});
}
}
testAllWebhooks();
```
---
Best Practices
1. Security
Always validate the API-Key header:
```typescript
// β INSECURE - Never skip validation
router.post('/webhook', async (req, res) => {
await handleEvent(req.body);
res.json({ message: 'ok' });
});
// β SECURE - Always verify secret
router.post('/webhook', async (req, res) => {
if (req.get('API-Key') !== process.env.PAYRAM_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'unauthorized' });
}
await handleEvent(req.body);
res.json({ message: 'ok' });
});
```
Additional security measures:
- Use HTTPS in production (Payram won't send webhooks to HTTP)
- Store webhook secret in environment variables
- Rotate secrets periodically
- Log failed authentication attempts
- Consider IP whitelisting if Payram provides static IPs
2. Idempotency
Handle duplicate webhook deliveries gracefully:
```typescript
async function handleFilledPayment(payload: PayramWebhookPayload) {
// Check if already processed
const existing = await db.payments.findUnique({
where: { payramReferenceId: payload.reference_id },
});
if (existing && existing.status === 'completed') {
console.log('Payment already processed, skipping:', payload.reference_id);
return; // Idempotent - safe to receive multiple times
}
// Process payment
await db.payments.update({
where: { payramReferenceId: payload.reference_id },
data: { status: 'completed', paidAt: new Date() },
});
await fulfillOrder(payload.customer_id, payload.reference_id);
}
```
3. Error Handling
Return appropriate HTTP status codes:
- 200: Webhook processed successfully
- 400: Invalid payload structure
- 401: Invalid API-Key
- 500: Internal processing error (Payram will retry)
Retry behavior: If you return 5xx, Payram will retry with exponential backoff.
```typescript
try {
await handlePayramEvent(payload);
return res.status(200).json({ message: 'success' });
} catch (error) {
console.error('Webhook processing failed:', error);
// Determine if retryable
if (error instanceof DatabaseConnectionError) {
// Temporary issue - return 500 to trigger retry
return res.status(500).json({ error: 'temporary_failure' });
} else {
// Permanent issue - return 200 to prevent retries
console.error('Permanent failure, not retrying:', error);
return res.status(200).json({ message: 'acknowledged' });
}
}
```
4. Logging
Log all webhook events for debugging:
```typescript
async function handlePayramEvent(payload: PayramWebhookPayload) {
// Log incoming event
console.log('Webhook received:', {
reference_id: payload.reference_id,
status: payload.status,
timestamp: new Date().toISOString(),
});
try {
await routeEventByStatus(payload);
// Log success
console.log('Webhook processed successfully:', payload.reference_id);
} catch (error) {
// Log error with context
console.error('Webhook processing failed:', {
reference_id: payload.reference_id,
status: payload.status,
error: error.message,
stack: error.stack,
});
throw error;
}
}
```
5. Database Transactions
Use transactions for critical operations:
```typescript
async function handleFilledPayment(payload: PayramWebhookPayload) {
await db.$transaction(async (tx) => {
// Update payment status
await tx.payments.update({
where: { payramReferenceId: payload.reference_id },
data: { status: 'completed' },
});
// Fulfill order
await tx.orders.update({
where: { paymentReferenceId: payload.reference_id },
data: { status: 'fulfilled', fulfilledAt: new Date() },
});
// Update inventory
await tx.inventory.decrement({
where: { orderId: order.id },
data: { quantity: order.quantity },
});
});
}
```
---
Troubleshooting
Webhook not received
Causes:
- Webhook URL not configured in Payram dashboard
- Endpoint not publicly accessible
- Firewall blocking Payram's IP addresses
- HTTPS certificate issues
Solutions:
- Verify webhook URL in Payram dashboard
- Use ngrok for local testing:
ngrok http 3000 - Check firewall rules
- Ensure valid SSL certificate (Let's Encrypt recommended)
- Test endpoint with curl from external server
Getting 401 errors in logs
Cause: API-Key header mismatch.
Solutions:
- Verify
PAYRAM_WEBHOOK_SECRETin.envmatches dashboard value - Check for extra whitespace in environment variable
- Ensure header name is exact:
API-Key(case-sensitive) - Restart server after updating
.env
Webhooks timing out
Cause: Handler taking too long to respond.
Solutions:
- Move heavy processing to background jobs
- Return 200 immediately, process asynchronously
- Use message queue (Redis, RabbitMQ) for processing
```typescript
router.post('/webhook', async (req, res) => {
const payload = req.body;
// Validate quickly
if (!validatePayload(payload)) {
return res.status(400).json({ error: 'invalid' });
}
// Queue for processing
await queue.add('process-webhook', payload);
// Respond immediately
return res.status(200).json({ message: 'queued' });
});
```
Duplicate events received
Cause: Payram retries on 5xx or timeout.
Solution: Implement idempotency (see Best Practices #2).
Missing status updates
Causes:
- Endpoint returned error
- Processing crashed before responding
- Webhook not configured for all events
Solutions:
- Check webhook logs in Payram dashboard
- Implement proper error handling
- Add comprehensive logging
- Use status polling as fallback
---
Related Skills
- setup-payram: Configure environment and credentials
- integrate-payments: Create payments that trigger webhooks
- integrate-payouts: Handle payout status webhooks
---
Summary
You now have complete webhook integration across all frameworks:
- Security: API-Key header validation for all requests
- Event routing: Status-based handlers for all payment states
- Framework handlers: Ready-to-use implementations for Express, Next.js, FastAPI, Laravel, Gin, Spring Boot
- Testing: Local testing tools for all webhook events
- Best practices: Idempotency, error handling, logging, transactions
Key Reminders:
- Always validate
API-Keyheader - Implement idempotency for all handlers
- Return 200 for successfully processed events
- Return 5xx only for retryable errors
- Use HTTPS in production
- Test locally with mock events before deploying
Next Steps:
- Implement webhook handler for your framework
- Configure webhook URL in Payram dashboard
- Set
PAYRAM_WEBHOOK_SECRETenvironment variable - Test with mock events locally
- Deploy and verify production webhooks
- Monitor webhook logs for issues
More from this repository10
Skill
Skill
Skill
Configures and validates connectivity to a self-hosted Payram server by setting up environment variables and testing API authentication.
Skill
Skill
Skill
Skill
Skill
Enables seamless cryptocurrency payment integration by creating checkouts, redirecting customers, and tracking payment statuses across multiple languages and frameworks.