payram-webhook-integration

Integrate PayRam webhook handlers for real-time payment and payout event notifications. Self-hosted, no-KYC crypto payment gateway webhooks. Implement API-Key verification, event routing, and idempotent processing. Generate handlers for Express, Next.js, FastAPI, Gin, Laravel, Spring Boot. Use when setting up payment confirmation callbacks, handling payout status updates, building event-driven payment flows, or integrating PayRam events into existing systems.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "payram-webhook-integration" with this command: npx skills add payram/payram-helper-mcp-server/payram-payram-helper-mcp-server-payram-webhook-integration

PayRam Webhook Setup

First time with PayRam? See payram-setup to configure your server, API keys, and wallets.

Receive real-time notifications when payments confirm, fail, or payouts complete. Webhooks eliminate polling and enable event-driven architectures.

Webhook Flow

1. Payment status changes on-chain
2. PayRam sends POST to your webhook URL
3. Your handler verifies API-Key header
4. Process event (fulfill order, update DB)
5. Return 200 OK

Configuring Webhooks in PayRam

  1. Navigate to Settings → Webhooks in PayRam dashboard
  2. Add your endpoint URL: https://your-app.com/api/payram/webhook
  3. Copy the shared webhook secret
  4. Store secret as PAYRAM_WEBHOOK_SECRET in your .env

Webhook Payload

PayRam sends webhook requests with an API-Key header for verification:

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"
}

Critical: Verify the API-Key header matches your stored PAYRAM_WEBHOOK_SECRET before processing.

Payment Status Events

StatusMeaning
OPENPayment created, awaiting customer action
FILLEDPayment completed successfully (exact amount paid)
PARTIALLY_FILLEDPartial payment received (less than requested)
OVER_FILLEDOverpayment received (more than requested)
CANCELLEDPayment cancelled by customer or merchant
UNDEFINEDUnknown status (future compatibility)

TypeScript Type Definitions

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;
  [key: string]: unknown;
}

Event Router

export async function handlePayramEvent(payload: PayramWebhookPayload) {
  switch (payload.status) {
    case 'FILLED':
      // Mark order as paid, deliver goods, send confirmation
      await fulfillOrder(payload.reference_id);
      break;
    case 'PARTIALLY_FILLED':
      // Update outstanding balance, notify finance team
      break;
    case 'OVER_FILLED':
      // Queue manual review or process refund
      break;
    case 'CANCELLED':
      // Release inventory, notify customer
      break;
    case 'OPEN':
      // Record payment acknowledgement
      break;
    default:
      // Log for investigation
      console.warn('Unknown status:', payload.status);
      break;
  }
}

Framework Handlers

Express.js

import express, { Request, Response } from 'express';
import crypto from 'crypto';

const router = express.Router();
router.use(express.json());

router.post('/api/payram/webhook', async (req: Request, res: Response) => {
  const sharedSecret = process.env.PAYRAM_WEBHOOK_SECRET;
  if (!sharedSecret) {
    return res.status(500).json({ error: 'webhook_not_configured' });
  }

  // Validate API-Key header (timing-safe comparison)
  const incomingKey = req.get('API-Key');
  if (!incomingKey) {
    return res.status(401).json({ error: 'invalid-webhook-key' });
  }

  const isValid = crypto.timingSafeEqual(Buffer.from(incomingKey), Buffer.from(sharedSecret));
  if (!isValid) {
    return res.status(401).json({ error: 'invalid-webhook-key' });
  }

  const payload = req.body;
  if (!payload?.reference_id || !payload?.status) {
    return res.status(400).json({ error: 'invalid-webhook-payload' });
  }

  try {
    await handlePayramEvent(payload);
    return res.json({ message: 'Webhook received successfully' });
  } catch (error) {
    console.error('Webhook handler error:', error);
    return res.status(500).json({ error: 'webhook_handler_error' });
  }
});

Next.js App Router

import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const sharedSecret = process.env.PAYRAM_WEBHOOK_SECRET;
  if (!sharedSecret) {
    return NextResponse.json({ error: 'webhook_not_configured' }, { status: 500 });
  }

  const incomingKey = request.headers.get('API-Key');
  if (!incomingKey) {
    return NextResponse.json({ error: 'invalid-webhook-key' }, { status: 401 });
  }

  const isValid = crypto.timingSafeEqual(Buffer.from(incomingKey), Buffer.from(sharedSecret));
  if (!isValid) {
    return NextResponse.json({ error: 'invalid-webhook-key' }, { status: 401 });
  }

  const payload = await request.json();
  if (!payload.reference_id || !payload.status) {
    return NextResponse.json({ error: 'invalid-webhook-payload' }, { status: 400 });
  }

  try {
    await handlePayramEvent(payload);
    return NextResponse.json({ message: 'Webhook received successfully' });
  } catch (error) {
    return NextResponse.json({ error: 'webhook_handler_error' }, { status: 500 });
  }
}

FastAPI (Python)

import os
import hmac
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()

@app.post('/api/payram/webhook')
async def payram_webhook(request: Request):
    shared_secret = os.getenv('PAYRAM_WEBHOOK_SECRET')
    if not shared_secret:
        raise HTTPException(status_code=500, detail='webhook_not_configured')

    incoming_key = request.headers.get('API-Key')
    if not incoming_key:
        raise HTTPException(status_code=401, detail='invalid-webhook-key')

    # Timing-safe comparison
    if not hmac.compare_digest(incoming_key, shared_secret):
        raise HTTPException(status_code=401, detail='invalid-webhook-key')

    payload = await request.json()
    if 'reference_id' not in payload or 'status' not in payload:
        raise HTTPException(status_code=400, detail='invalid-webhook-payload')

    await handle_payram_event(payload)
    return {'message': 'Webhook received successfully'}

Gin (Go)

import (
    "crypto/subtle"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

func handlePayramWebhook(c *gin.Context) {
    sharedSecret := os.Getenv("PAYRAM_WEBHOOK_SECRET")
    if sharedSecret == "" {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "webhook_not_configured"})
        return
    }

    incomingKey := c.GetHeader("API-Key")
    if incomingKey == "" {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid-webhook-key"})
        return
    }

    // Timing-safe comparison
    if subtle.ConstantTimeCompare([]byte(incomingKey), []byte(sharedSecret)) != 1 {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid-webhook-key"})
        return
    }

    var payload PayramWebhookPayload
    if err := c.ShouldBindJSON(&payload); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid-json-payload"})
        return
    }

    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"})
}

Laravel (PHP)

class PayramWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $sharedSecret = env('PAYRAM_WEBHOOK_SECRET');
        if (!$sharedSecret) {
            return response()->json(['error' => 'webhook_not_configured'], 500);
        }

        $incomingKey = $request->header('API-Key');
        if (!$incomingKey) {
            return response()->json(['error' => 'invalid-webhook-key'], 401);
        }

        // Timing-safe comparison
        if (!hash_equals($sharedSecret, $incomingKey)) {
            return response()->json(['error' => 'invalid-webhook-key'], 401);
        }

        $payload = $request->json()->all();
        if (empty($payload['reference_id']) || empty($payload['status'])) {
            return response()->json(['error' => 'invalid-webhook-payload'], 400);
        }

        $this->router->handle($payload);
        return response()->json(['message' => 'Webhook received successfully']);
    }
}

Spring Boot (Java)

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

@PostMapping("/webhook")
public ResponseEntity<?> handleWebhook(
        @RequestBody Map<String, Object> payload,
        @RequestHeader(value = "API-Key", required = false) String apiKey) {

    String sharedSecret = System.getenv("PAYRAM_WEBHOOK_SECRET");
    if (sharedSecret == null || sharedSecret.isBlank()) {
        return ResponseEntity.status(500).body(Map.of("error", "webhook_not_configured"));
    }

    if (apiKey == null || apiKey.isBlank()) {
        return ResponseEntity.status(401).body(Map.of("error", "invalid-webhook-key"));
    }

    // Timing-safe comparison
    boolean isValid = MessageDigest.isEqual(
        apiKey.getBytes(StandardCharsets.UTF_8),
        sharedSecret.getBytes(StandardCharsets.UTF_8)
    );
    if (!isValid) {
        return ResponseEntity.status(401).body(Map.of("error", "invalid-webhook-key"));
    }

    if (!payload.containsKey("reference_id") || !payload.containsKey("status")) {
        return ResponseEntity.status(400).body(Map.of("error", "invalid-webhook-payload"));
    }

    router.handle(payload);
    return ResponseEntity.ok(Map.of("message", "Webhook received successfully"));
}

Best Practices

Idempotency: Handle duplicate deliveries gracefully — check if already processed before fulfilling:

async function handleFilledPayment(payload: PayramWebhookPayload) {
  const existing = await db.payments.findUnique({
    where: { payramReferenceId: payload.reference_id },
  });
  if (existing && existing.status === 'completed') {
    return; // Already processed, safe to skip
  }
  await db.payments.update({
    where: { payramReferenceId: payload.reference_id },
    data: { status: 'completed', paidAt: new Date() },
  });
  await fulfillOrder(payload.customer_id, payload.reference_id);
}

Quick Response: Return 200 immediately, process asynchronously. PayRam retries on timeout.

Retry Handling: If you return 5xx, PayRam retries with exponential backoff. Return 200 for permanent failures to prevent retries.

Database Transactions: Use transactions for critical operations to ensure consistency.

Testing Webhooks

cURL Test

curl -X POST http://localhost:3000/api/payram/webhook \
  -H "Content-Type: application/json" \
  -H "API-Key: $PAYRAM_WEBHOOK_SECRET" \
  -d '{
    "reference_id": "ref_test_001",
    "status": "FILLED",
    "customer_id": "cust_123",
    "amount": 49.99,
    "filled_amount_in_usd": 49.99,
    "currency": "USD"
  }'

MCP Server Tools

ToolPurpose
generate_webhook_handlerFramework-specific handler code
generate_webhook_event_routerFan-out router for multiple event types
generate_mock_webhook_eventTest payloads for each event type

Environment Variables

PAYRAM_WEBHOOK_SECRET=your-webhook-secret-from-dashboard

All PayRam Skills

SkillWhat it covers
payram-setupServer config, API keys, wallet setup, connectivity test
payram-agent-onboardingAgent onboarding — CLI-only deployment for AI agents, no web UI
payram-analyticsAnalytics dashboards, reports, and payment insights via MCP tools
payram-crypto-paymentsArchitecture overview, why PayRam, MCP tools
payram-payment-integrationQuick-start payment integration guide
payram-self-hosted-payment-gatewayDeploy and own your payment infrastructure
payram-checkout-integrationCheckout flow with SDK + HTTP for 6 frameworks
payram-webhook-integrationWebhook handlers for Express, Next.js, FastAPI, Gin, Laravel, Spring Boot
payram-stablecoin-paymentsUSDT/USDC acceptance across EVM chains and Tron
payram-bitcoin-paymentsBTC with HD wallet derivation and mobile signing
payram-payoutsSend crypto payouts and manage referral programs
payram-no-kyc-crypto-paymentsNo-KYC, no-signup, permissionless payment acceptance

Support

Need help? Message the PayRam team on Telegram: @PayRamChat

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

payram-crypto-payments

No summary provided by upstream source.

Repository SourceNeeds Review
Web3

payram-no-kyc-crypto-payments

No summary provided by upstream source.

Repository SourceNeeds Review
Web3

compare-crypto-payments

No summary provided by upstream source.

Repository SourceNeeds Review
General

payram-setup

No summary provided by upstream source.

Repository SourceNeeds Review