Callbacks

Receive real-time notifications about transaction status changes.

Overview

PayInn sends HTTP POST requests to your callback URL whenever a transaction status changes. These callbacks contain the transaction details and a signature for verification.

Important

You may receive multiple callbacks for the same transaction (e.g., status changes from pending to processing to completed). Make sure to handle callbacks idempotently - process each unique status change only once.

Callback URL Configuration

Your callback URL is configured via the admin panel when you register with PayInn.

RequirementDescription
ProtocolHTTPS only (SSL/TLS required)
MethodPOST
Content-Typeapplication/json
ResponseReturn 2xx status code to acknowledge receipt
Timeout30 seconds

Callback Headers

Each callback includes these headers for verification:

HeaderDescription
X-SignatureHMAC-SHA256 signature
X-TimestampUnix timestamp in seconds
Content-Typeapplication/json
User-AgentPayInn-Callback/1.0

Deposit Callback Payload

{
  "transactionId": "TXN-abc123def456",
  "processId": "ORDER-12345",
  "type": "deposit",
  "status": "completed",
  "amount": 1000,
  "currency": "TRY",
  "completedAt": "2024-01-15T12:15:00.000Z",
  "timestamp": 1705320900
}

Deposit Callback Fields

FieldTypeDescription
transactionIdstringPayInn transaction ID
processIdstringYour transaction ID
typestringTransaction type: deposit or withdrawal
statusstringTransaction status: completed or failed
amountnumberConfirmed transaction amount (may differ from requested)
currencystringCurrency code
completedAtstringCompletion time (ISO 8601) - only for completed status
failureReasonstringReason for failure - only for failed status
timestampnumberCallback timestamp

Withdrawal Callback Payload

{
  "transactionId": "TXN-xyz789abc123",
  "processId": "WITHDRAW-12345",
  "type": "withdrawal",
  "status": "completed",
  "amount": 5000,
  "currency": "TRY",
  "completedAt": "2024-01-15T12:30:00.000Z",
  "timestamp": 1705321800
}

Signature Verification

Always verify the callback signature to ensure the request is from PayInn. The signature is calculated using HMAC-SHA256 with your Callback Secret.

signature = HMAC-SHA256(rawRequestBody, callbackSecret)

Use Raw Request Body

Always use the raw HTTP request body string for signature verification. Do NOT parse the JSON and re-serialize it, as different languages/frameworks may format numbers differently (e.g., 550 vs 550.0), causing signature mismatches.
Node.js Verification Example
const crypto = require('crypto');
const express = require('express');
const app = express();

const CALLBACK_SECRET = 'your-callback-secret';

// Capture raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

// Callback endpoint
app.post('/webhook/payinn', (req, res) => {
  const signature = req.headers['x-signature'];

  // IMPORTANT: Use raw body for signature verification
  // Do NOT use JSON.stringify(req.body) as it may change number formatting
  const expectedSignature = crypto
    .createHmac('sha256', CALLBACK_SECRET)
    .update(req.rawBody)
    .digest('hex');

  if (signature !== expectedSignature) {
    console.error('Invalid signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Use parsed body for processing
  const payload = req.body;
  console.log('Received callback:', payload);

  switch (payload.status) {
    case 'completed':
      // Credit the user's balance
      creditUserBalance(payload.processId, payload.amount);
      break;

    case 'failed':
      // Handle failed transaction
      handleFailedTransaction(payload.processId, payload.failureReason);
      break;
  }

  // Acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3000);
PHP Verification Example
<?php

$callbackSecret = 'your-callback-secret';

// Get headers and body
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);

// Verify signature
$expectedSignature = hash_hmac('sha256', $payload, $callbackSecret);

if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process the callback
switch ($data['status']) {
    case 'completed':
        // Credit the user's balance
        creditUserBalance($data['processId'], $data['amount']);
        break;

    case 'failed':
        // Handle failed transaction
        handleFailedTransaction($data['processId'], $data['failureReason'] ?? null);
        break;
}

// Acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);
C# Verification Example
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;

[ApiController]
[Route("webhook")]
public class PayInnCallbackController : ControllerBase
{
    private const string CallbackSecret = "your-callback-secret";

    [HttpPost("payinn")]
    public async Task<IActionResult> HandleCallback()
    {
        // IMPORTANT: Read raw body for signature verification
        // Do NOT use model binding as it may change JSON formatting
        using var reader = new StreamReader(Request.Body);
        var rawBody = await reader.ReadToEndAsync();

        var signature = Request.Headers["X-Signature"].FirstOrDefault();

        // Verify signature using raw body
        var expectedSignature = ComputeHmacSha256(rawBody, CallbackSecret);

        if (signature != expectedSignature)
        {
            return Unauthorized(new { error = "Invalid signature" });
        }

        // Parse body for processing
        var payload = System.Text.Json.JsonSerializer.Deserialize<CallbackPayload>(rawBody);

        switch (payload.Status)
        {
            case "completed":
                // Credit the user's balance
                await CreditUserBalance(payload.ProcessId, payload.Amount);
                break;
            case "failed":
                // Handle failed transaction
                await HandleFailedTransaction(payload.ProcessId, payload.FailureReason);
                break;
        }

        return Ok(new { received = true });
    }

    private static string ComputeHmacSha256(string data, string secret)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
        return Convert.ToHexString(hash).ToLower();
    }
}

Retry Policy

If your endpoint doesn't respond with a 2xx status code, PayInn will retry the callback:

AttemptDelay
1st retry5 minutes
2nd retry15 minutes
3rd retry1 hour

Best Practices

  • Always verify the signature before processing
  • Return 200 OK as quickly as possible
  • Process the callback asynchronously if needed
  • Handle duplicate callbacks idempotently
  • Log all callbacks for debugging

Transaction Statuses

StatusDescriptionAction Required
completedTransaction completed successfullyCredit user balance / mark order as paid
failedTransaction failedShow error to user, allow retry